Compare commits

...

190 Commits

Author SHA1 Message Date
Mauricio Siu
a6de744f91 fix(2fa): improve error handling and display for two-factor authentication setup
- Handle potential errors when enabling two-factor authentication
- Provide more detailed error messages for password verification
- Update client-side error handling to display specific error messages
- Add base URL configuration for non-cloud environments
2025-03-08 12:37:50 -06:00
Mauricio Siu
b64ddf1119 Merge pull request #1392 from ali-issa/feature/project-view-tab-reorg
feat: reorganize project view tabs into logical workflow groups #1261
2025-03-08 12:36:46 -06:00
Mauricio Siu
2f074ac734 Merge pull request #1405 from frostming/patch-1
feat: fallback to openai compatible provider if url host doesn't match
2025-03-07 00:56:33 -06:00
Mauricio Siu
96e3721b4b chore(ai): remove debug logging in model fetching 2025-03-07 00:56:12 -06:00
Mauricio Siu
b8e5cae88f feat(ai): improve model fetching and error handling
- Add server-side model fetching endpoint with flexible provider support
- Refactor client-side AI settings component to use new API query
- Implement dynamic header generation for different AI providers
- Enhance error handling and toast notifications
- Remove local model fetching logic in favor of server-side implementation
2025-03-07 00:55:11 -06:00
Mauricio Siu
fa20444a14 Merge pull request #1414 from gentslava/fix/template-superset
fix(template): superset
2025-03-07 00:12:43 -06:00
Mauricio Siu
668ccabec8 Merge pull request #1390 from hexaaagon/feat/zipline
feat(zipline): update zipline version
2025-03-07 00:12:19 -06:00
Mauricio Siu
aa07a0c574 Merge pull request #1417 from vinumzz/vinumzz-remove-shadow-monitoring
refactor: remove unnecessary extra shadow from monitoring page
2025-03-07 00:10:22 -06:00
Mauricio Siu
0b64b43376 Merge pull request #1415 from gentslava/feature/template-datalens
feat(template): DataLens
2025-03-07 00:10:03 -06:00
Mauricio Siu
5c65dc9a21 Update apps/dokploy/templates/datalens/docker-compose.yml 2025-03-07 00:06:20 -06:00
Mauricio Siu
58262606d4 Update apps/dokploy/templates/datalens/docker-compose.yml 2025-03-07 00:06:16 -06:00
Mauricio Siu
f73959db41 Update apps/dokploy/templates/datalens/docker-compose.yml 2025-03-07 00:06:10 -06:00
Mauricio Siu
e6c664e65f Update apps/dokploy/templates/datalens/docker-compose.yml 2025-03-07 00:06:06 -06:00
Mauricio Siu
36cc157566 Merge pull request #1355 from drudge/templates/hoarder
feat: add Hoarder template
2025-03-07 00:03:42 -06:00
Mauricio Siu
7e070623cc Merge pull request #1411 from gentslava/canary
fix: breadcrumbs UX
2025-03-06 23:58:42 -06:00
Mauricio Siu
b2c0a685f8 fix(destinations): validate server selection for cloud destinations 2025-03-06 23:57:39 -06:00
Mauricio Siu
c14528886d Merge pull request #1424 from Dokploy/1382-automated-postgres-backup-always-empty
feat(destinations): add createdAt timestamp and display creation date
2025-03-06 23:54:03 -06:00
Mauricio Siu
29eb490e2d feat(destinations): add createdAt timestamp and display creation date 2025-03-06 23:46:21 -06:00
Mauricio Siu
6166963b00 fix(gitlab): remove debug console logs from connection testing 2025-03-06 22:27:30 -06:00
Mauricio Siu
f544efed35 Merge pull request #1422 from Dokploy/1418-fetching-gitlab-repositories-0-found
fix(gitlab): update repository filtering and connection testing
2025-03-06 22:17:34 -06:00
Mauricio Siu
598d095241 fix(gitlab): update repository filtering and connection testing
- Change repository filtering to use 'user' kind instead of 'member'
- Add console logging for debugging GitLab provider and repository connection
- Ensure consistent filtering logic in both getGitlabRepositories and testGitlabConnection
2025-03-06 22:17:20 -06:00
Mauricio Siu
457a8e05fd chore(issue-template): update bug report label emoji 2025-03-06 22:05:31 -06:00
Mauricio Siu
3ca057c44a chore(issue-template): update bug report label and add cloud version option 2025-03-06 22:04:46 -06:00
Nicholas Penree
ad3a0198e9 feat: add Hoarder template 2025-03-06 08:23:49 -05:00
Peter Vinum
ab5f62604c refactor: remove unnecessary extra shadow from monitoring page 2025-03-06 14:08:23 +01:00
Vyacheslav Shcherbinin
bf9e886b9a Disable demo 2025-03-06 14:14:01 +07:00
Vyacheslav Shcherbinin
f5cd0fbdd8 Restart policy 2025-03-06 11:32:13 +07:00
Vyacheslav Scherbinin
8859cc97b4 fix: superset docker-compose 2025-03-06 10:46:10 +07:00
Vyacheslav Scherbinin
3bdd5e4dd0 Template DataLens 2025-03-06 10:34:13 +07:00
Vyacheslav Scherbinin
b0c710aa92 Tab instead space 2025-03-05 21:25:11 +07:00
Vyacheslav Scherbinin
c83d0a95b7 Remove ending separator 2025-03-05 20:53:38 +07:00
Vyacheslav Scherbinin
71ca5babfd Remove duplicate breadcrumb 2025-03-05 20:51:09 +07:00
Vyacheslav Scherbinin
f342613503 Text format 2025-03-05 20:27:16 +07:00
Frost Ming
efd176451f fix: default case correct 2025-03-05 16:19:28 +08:00
Frost Ming
a7fd64e019 fix: use a named case 2025-03-05 16:17:50 +08:00
Frost Ming
21c8b98f9c feat: fallback to openai compatible provider if url host doesn't match 2025-03-05 16:12:46 +08:00
Mauricio Siu
6ff06576d0 fix(dockerfile): update Railpack installation script to use bash 2025-03-05 00:36:20 -06:00
Mauricio Siu
24cc08a1ac Merge pull request #1402 from Dokploy/feat/add-railpack
feat(application): add Railpack as a new build type
2025-03-05 00:24:37 -06:00
Mauricio Siu
e039826d50 chore(version): bump project version to v0.19.1 2025-03-05 00:21:36 -06:00
Mauricio Siu
a947b4915a Merge pull request #1398 from shaunjanssens/feature/bitbucket-branches-length
feat: Bitbucket branch length
2025-03-05 00:19:25 -06:00
Mauricio Siu
fc1d9ad202 Merge pull request #1399 from nktnet1/profile-moved-to-top
feat(ui): move profile to top of account dropdown
2025-03-05 00:18:43 -06:00
Mauricio Siu
5489e3b0a5 feat(application): add Railpack as a new build type
- Introduce Railpack as a new build method for applications
- Update database schema to include 'railpack' in buildType enum
- Add Railpack installation and validation scripts for servers
- Implement Railpack build and command generation utilities
- Update UI to include Railpack as a build option with a 'New' badge
2025-03-05 00:18:10 -06:00
Khiet Tam Nguyen
e43b907a8d feat(ui): move profile to first item in account dropdown 2025-03-04 22:42:04 +11:00
Shaun Janssens
62fae661a1 feat: bitbucket branch length 2025-03-04 10:50:42 +01:00
Ali Issa
6846e0e5a3 feat: reorganize project view tabs into logical workflow groups #1261
Restructure the project view tabs to follow a more intuitive user journey:
- Group tabs into "Initial Setup", "Deployment", and "Monitoring" sections
- Maintain "Advanced" as a standalone option
- Order tabs to match typical project workflow (configuration → deployment → monitoring)

This reorganization reduces cognitive load by grouping related functions,
minimizes tab switching during common tasks, and provides a clearer
mental model of the platform's workflow for new users.
2025-03-03 06:45:36 -05:00
Hexaa
49d4cea06f feat(zipline): update zipline version 2025-03-03 12:17:42 +07:00
Mauricio Siu
5db7508530 feat(organization): prevent deletion of last owned organization 2025-03-02 22:32:56 -06:00
Mauricio Siu
4da4e1c17d feat(side-menu): restrict AI settings to owner role 2025-03-02 22:16:10 -06:00
Mauricio Siu
126dad498c chore(version): bump project version to v0.19.0 2025-03-02 21:59:31 -06:00
Mauricio Siu
9c74b18e37 Merge pull request #1387 from Dokploy/1347-see-all-the-10-last-deployments-for-this-compose-show-all
feat(deployments): improve deployment list display and cleanup logic
2025-03-02 03:52:44 -06:00
Mauricio Siu
b13b906d75 feat(deployments): improve deployment list display and cleanup logic
- Add deployment index numbering in the UI
- Adjust deployment cleanup to retain 9 instead of 10 deployments
- Update deployment list rendering to show numbered status
2025-03-02 03:52:29 -06:00
Mauricio Siu
f63d582530 chore(dependencies): add Ollama AI provider to project dependencies
- Integrate Ollama AI provider in package.json
- Update pnpm-lock.yaml with Ollama AI provider dependency
2025-03-02 03:35:32 -06:00
Mauricio Siu
0fa728d905 chore(dependencies): add AI SDK providers to project dependencies
- Integrate multiple AI SDK providers including Anthropic, Azure, Cohere, DeepInfra, Mistral, and OpenAI Compatible
- Update package.json and pnpm-lock.yaml with new AI SDK dependencies
2025-03-02 03:22:11 -06:00
Mauricio Siu
b56272871f Merge pull request #1384 from Dokploy/mauricio/feature
Feat/AI
2025-03-02 02:31:24 -06:00
Mauricio Siu
1ffdebf60b Merge branch 'canary' into mauricio/feature 2025-03-02 02:16:45 -06:00
Mauricio Siu
0e81a027c1 Merge pull request #1385 from Dokploy/fix/migration
fix(database): ensure adminId is not null for certificates, notificat…
2025-03-02 02:16:06 -06:00
Mauricio Siu
cf3b3a2dcd fix(database): ensure adminId is not null for certificates, notifications, ssh-keys, and git providers 2025-03-02 02:15:49 -06:00
Mauricio Siu
a8fc27e830 feat(ai): add configuration files support for AI template generation
- Enhance template generation with configFiles feature
- Update StepTwo and StepThree components to display and edit configuration files
- Modify AI router and schemas to support configuration file mounting
- Refine AI service prompt to provide stricter guidelines for config file usage
2025-03-02 01:54:39 -06:00
Mauricio Siu
e7db3a196c feat(ai): enhance AI service prompt generation with strict guidelines
- Update project suggestion prompt to enforce plain text descriptions
- Add detailed rules for description, shortDescription, and response format
- Improve docker compose generation guidelines with comprehensive configuration rules
- Specify strict requirements for environment variables, volumes, and config files
2025-03-02 01:35:34 -06:00
Mauricio Siu
f78cda9cce feat(ai): update AI settings to use organization-based access control
- Refactor AI-related schemas and services to use organizationId instead of adminId
- Update AI router to check organization-level permissions
- Modify AI settings creation and retrieval to work with organization context
- Adjust server-side props and access checks for AI settings
2025-03-02 00:54:46 -06:00
Mauricio Siu
747c2137c9 Reapply "Merge branch 'canary' into kucherenko/canary"
This reverts commit e6cb6454db.
2025-03-02 00:30:02 -06:00
Mauricio Siu
e6cb6454db Revert "Merge branch 'canary' into kucherenko/canary"
This reverts commit 819822f30b, reversing
changes made to bda9b05134.
2025-03-02 00:26:59 -06:00
Mauricio Siu
819822f30b Merge branch 'canary' into kucherenko/canary 2025-03-02 00:23:58 -06:00
Mauricio Siu
5b3005eb89 Merge pull request #1376 from 190km/templates/listmonk-uploads
fix(templates): listmonk needed files volumes
2025-03-01 23:19:12 -06:00
Mauricio Siu
85a13eed00 Update apps/dokploy/templates/listmonk/docker-compose.yml 2025-03-01 23:19:02 -06:00
Mauricio Siu
e1b94dfe5b Merge pull request #1314 from vicke4/canary
feat: notifications to specific Telegram topics
2025-03-01 23:16:31 -06:00
Mauricio Siu
948fdbc22b chore(auth): remove default credentials from login and register forms 2025-03-01 23:09:43 -06:00
Mauricio Siu
eed38860b9 feat(database): add messageThreadId column to telegram table 2025-03-01 23:08:53 -06:00
Mauricio Siu
fefb5d14e0 Merge branch 'canary' into vicke4/canary 2025-03-01 23:05:30 -06:00
Mauricio Siu
8946f68af9 Merge pull request #1351 from Dokploy/feat/better-auth-2
Feat/Organizations & Better auth
2025-03-01 23:00:10 -06:00
Mauricio Siu
5fb2866660 feat(auth): conditionally disable authentication logger in production 2025-03-01 22:48:09 -06:00
Mauricio Siu
c51d63a4df chore: remove TODO comments and clean up code annotations 2025-03-01 22:21:03 -06:00
Mauricio Siu
13eccaf8d9 feat(user): add organization count check before user deletion 2025-03-01 22:14:12 -06:00
Mauricio Siu
adeb8498f9 refactor(auth): remove commented-out debug logging in TRPC context 2025-03-01 21:56:22 -06:00
Mauricio Siu
43599e7a97 fix(auth): handle null session and user with TypeScript ignore 2025-03-01 21:54:47 -06:00
Mauricio Siu
d7c94174b9 refactor(auth): simplify API key export in authentication module 2025-03-01 21:49:56 -06:00
Mauricio Siu
5c38a8265f feat(auth): improve email verification and invitation link generation for cloud environment 2025-03-01 21:47:31 -06:00
Mauricio Siu
a3362e0b15 feat(auth): add Google sign-in support for cloud environment 2025-03-01 21:30:30 -06:00
Mauricio Siu
0ad9233087 feat(logs): enable dynamic log rotation with database state management 2025-03-01 20:55:18 -06:00
Mauricio Siu
5dc5292928 feat(api): implement advanced API key management with granular controls 2025-03-01 19:58:15 -06:00
Mauricio Siu
5568629839 refactor: organize imports and improve template utility modules 2025-03-01 14:45:50 -06:00
190km
5aff345aa8 feat: added uploads volume for images upload 2025-02-26 22:36:03 +01:00
Mauricio Siu
37ca8f41f5 refactor(ui): remove loading text in sidebar layout 2025-02-25 23:37:22 -06:00
Mauricio Siu
cbec0603bd feat(ui): add loading state to sidebar layout 2025-02-25 23:36:53 -06:00
Mauricio Siu
8c2707c4ea refactor(organization): migrate to react-hook-form with zod validation 2025-02-25 23:33:26 -06:00
Mauricio Siu
7d77e14319 feat(organization): add logo support for organizations 2025-02-25 23:28:19 -06:00
Mauricio Siu
d3c59ff8af refactor(ui): enhance sidebar layout with responsive design and collapsed state 2025-02-25 23:13:55 -06:00
Mauricio Siu
7048e7e37e Merge branch 'canary' into feat/better-auth-2 2025-02-25 23:05:57 -06:00
Mauricio Siu
0a6382a731 Merge pull request #1360 from Dokploy/remove-permit-root-login-advice
Remove permit root login advice
2025-02-23 18:11:58 -06:00
Mauricio Siu
d3b2cee7fb refactor(ui): improve log highlighting and template formatting 2025-02-23 18:11:27 -06:00
Mauricio Siu
125e44812b refactor(security): remove permit root login status row 2025-02-23 18:11:21 -06:00
Mauricio Siu
ac3378ccb8 feat(sponsors): add Synexa to sponsors section 2025-02-23 15:38:25 -06:00
Mauricio Siu
81e1161ba0 Merge pull request #1340 from wish-oss/glance-template
refactor: update glance template
2025-02-23 15:05:50 -06:00
Mauricio Siu
9aa13c5ac3 Merge pull request #1356 from drudge/templates/pocket-id
feat: add Pocket ID template
2025-02-23 14:51:53 -06:00
Mauricio Siu
398fd54815 Merge pull request #1354 from drudge/templates/linkwarden
feat: add Linkwarden template
2025-02-23 14:46:41 -06:00
Mauricio Siu
7f4e4ab8d2 Merge pull request #1346 from Cohvir/canary
feat(template): add Wiki.js
2025-02-23 14:36:35 -06:00
Mauricio Siu
211697acaf Merge branch 'canary' into canary 2025-02-23 14:36:13 -06:00
Mauricio Siu
c0b64c6e55 Update apps/dokploy/templates/wikijs/docker-compose.yml 2025-02-23 14:35:39 -06:00
Mauricio Siu
5871a91da5 Update apps/dokploy/templates/wikijs/docker-compose.yml 2025-02-23 14:35:33 -06:00
Mauricio Siu
f4d13c3030 Update apps/dokploy/templates/wikijs/docker-compose.yml 2025-02-23 14:35:28 -06:00
Mauricio Siu
e00e19ec01 Merge pull request #1341 from drudge/templates/mailpit
feat: add Mailpit template
2025-02-23 14:30:38 -06:00
Mauricio Siu
c995268b39 Merge pull request #1349 from 190km/fix/logs-highlight-search-terms
fix: fixed highligh search terms color
2025-02-23 14:29:22 -06:00
Mauricio Siu
c8828b5620 Merge pull request #1357 from sondreal/fix-outline-typo
fixes typo outline->getoutline #1352
2025-02-23 14:10:12 -06:00
Mauricio Siu
ddd3101aeb Merge pull request #1348 from skyfall-sh/update-umami
chore: update umami to v2.16.1
2025-02-23 14:09:36 -06:00
sondreal
51310dae1d fixed the links 2025-02-23 19:15:17 +01:00
sondreal
0b7996adde removed my linting 2025-02-23 19:13:42 +01:00
sondreal
fb4b507250 fixes typo outline->getoutline #1352 2025-02-23 19:03:46 +01:00
Nicholas Penree
1294c2ad8e feat: add Pocket ID template 2025-02-23 12:07:54 -05:00
Nicholas Penree
733f9a0024 feat: add Linkwarden template 2025-02-23 10:51:50 -05:00
Mauricio Siu
73d3b58867 feat: add GitHub sign-in option for cloud environment 2025-02-23 01:59:00 -06:00
Mauricio Siu
0ea138571d refactor: update auth module to separate handler and API 2025-02-23 00:35:08 -06:00
Mauricio Siu
b1e7ffea21 chore: enable TypeScript size limit bypass in server tsconfig 2025-02-23 00:22:35 -06:00
Mauricio Siu
c0a7347ef5 chore: remove additional TypeScript configuration options in server tsconfig 2025-02-23 00:22:17 -06:00
Mauricio Siu
579faf2f58 chore: adjust TypeScript configuration in server tsconfig 2025-02-23 00:20:27 -06:00
Mauricio Siu
7429a1f65f chore: enable TypeScript declaration generation in server tsconfig 2025-02-23 00:16:51 -06:00
Mauricio Siu
716c1db799 Revert "chore: disable TypeScript declaration generation in server tsconfig"
This reverts commit 87836d23c3.
2025-02-23 00:13:35 -06:00
Mauricio Siu
9dd7f51eeb chore: disable TypeScript declaration generation in schedules tsconfig 2025-02-23 00:07:38 -06:00
Mauricio Siu
4a1a5a9bb1 chore: comment out database schema definitions in auth-schema 2025-02-23 00:04:24 -06:00
Mauricio Siu
87836d23c3 chore: disable TypeScript declaration generation in server tsconfig 2025-02-22 23:25:22 -06:00
Mauricio Siu
30cbad93d2 refactor: improve Traefik error handling in service initialization 2025-02-22 23:22:15 -06:00
Mauricio Siu
038b021043 Merge branch 'canary' into feat/better-auth-2 2025-02-22 23:22:11 -06:00
Mauricio Siu
0ec8e2baa1 chore: update GitHub workflow branch trigger for authentication feature branch 2025-02-22 23:20:27 -06:00
Mauricio Siu
3f2722f28d refactor: remove unused imports and simplify auth router 2025-02-22 22:38:38 -06:00
Mauricio Siu
47f7648cb3 feat: improve user profile update and password change functionality
This commit adds enhanced password change validation and handling:

- Add password change validation in user update route
- Implement password verification before allowing changes
- Update user schema to support optional password fields
- Fix token display in generate token component
- Disable migration script temporarily
2025-02-22 22:37:57 -06:00
Mauricio Siu
0478419f7c refactor: migrate authentication routes to user router and update related components
This commit continues the refactoring of authentication-related code by:

- Moving authentication routes from `auth.ts` to `user.ts`
- Updating import paths and function calls across components
- Removing commented-out authentication code
- Simplifying user-related queries and mutations
- Updating server-side authentication handling
2025-02-22 22:02:12 -06:00
Mauricio Siu
b00c12965a refactor: update reset password and authentication flows
This commit removes several authentication-related components and simplifies the password reset process:

- Removed login-2fa component
- Deleted confirm-email page
- Updated reset password logic to use Drizzle ORM directly
- Removed unused authentication-related functions
- Simplified server-side authentication routes
2025-02-22 21:09:21 -06:00
Mauricio Siu
8ab6d6b282 chore: clean up unused variables and improve error handling across codebase
This commit focuses on removing unused variables, adding placeholder error handling, and generally tidying up various files across the Dokploy application. Changes include:

- Removing unused imports and variables
- Adding placeholder error handling in catch blocks
- Cleaning up commented-out code
- Removing deprecated utility files
- Improving type safety and code consistency
2025-02-22 20:35:21 -06:00
190km
2470d672d4 fix: fixed highligh search terms color 2025-02-23 01:18:18 +00:00
Mahad Kalam
3403f8ab36 chore: update umami to v2.16.1 2025-02-23 00:47:04 +00:00
Mauricio Siu
1a415b96c9 refactor: remove unused auth service and clean up server-side code 2025-02-22 18:03:12 -06:00
Mauricio Siu
81a881b07e feat: enhance organization invitation UI and add organization details 2025-02-22 13:53:57 -06:00
Cohvir
baf555af52 feat(template): add Wiki.js 2025-02-22 14:16:14 +01:00
Mauricio Siu
c52725420e refactor: use organization context for server creation 2025-02-22 02:35:44 -06:00
Mauricio Siu
b02195db17 feat: add organization invitation system and update user profile management 2025-02-22 02:31:04 -06:00
Mauricio Siu
5ae103e779 refactor: update permission checks to use organization context 2025-02-21 00:48:04 -06:00
Mauricio Siu
a317f0c4cc refactor: simplify user permission checks across application 2025-02-21 00:40:35 -06:00
Mauricio Siu
24c9d3f7ad refactor: update user permissions and API queries 2025-02-21 00:30:55 -06:00
Mauricio Siu
63638bde33 refactor: consolidate database migration and clean up legacy user tables 2025-02-21 00:07:36 -06:00
Mauricio Siu
725bd1a381 refactor: migrate permissions from user_temp to member table 2025-02-21 00:00:22 -06:00
Nicholas Penree
c8b1fd36bd feat: add Mailpit template 2025-02-19 22:29:36 -05:00
vishalkadam47
609fea7daa refactor: update glance template 2025-02-20 08:35:14 +05:30
Mauricio Siu
56af89468a Merge pull request #1313 from nktnet1/superset-networks
docs(template): note on networking for superset
2025-02-16 21:34:40 -06:00
Mauricio Siu
b1502f5f82 Merge pull request #1329 from drudge/templates/registry
feat(template): add docker registry template
2025-02-16 21:33:54 -06:00
Mauricio Siu
c78b8cfead Update package.json 2025-02-16 21:30:57 -06:00
Nicholas Penree
48c4ec55f9 fix(template): switch outline to png for dark mode 2025-02-16 15:52:15 -05:00
Nicholas Penree
2ebb02dc20 feat(template): add docker registry template 2025-02-15 22:46:37 -05:00
Mauricio Siu
9ca3ab3845 Merge pull request #1327 from Krobys/canary
feat(template): Add Appwrite template
2025-02-15 16:00:19 -06:00
Krobys
0baf9d8962 feat(template): add Appwrite 2025-02-15 21:46:18 +00:00
Mauricio Siu
71c609df0e Merge pull request #1326 from Dokploy/1304-preview-deployments-dont-work-when-enabling-auth-or-redirects
fix: inherit security & redirects from application
2025-02-15 15:24:36 -06:00
Mauricio Siu
450302b2c2 fix: inherit security & redirects from application 2025-02-15 15:23:33 -06:00
Mauricio Siu
71a007a4b3 Merge pull request #1325 from Dokploy/1257-main-server-error-error-http-code-409-unexpected---rpc-error
fix: handle race condition to catch recreation base containers
2025-02-15 14:26:52 -06:00
Mauricio Siu
871931b460 fix: handle race condition to catch recreation base containers 2025-02-15 14:23:54 -06:00
Mauricio Siu
3a7bb5016c Merge pull request #1324 from Dokploy/1320-missing-path-option-when-adding-a-template-in-dokploy
refactor: add missing path option
2025-02-15 14:09:33 -06:00
Mauricio Siu
23b59076b8 refactor: add missing path option 2025-02-15 13:35:25 -06:00
Mauricio Siu
2ddfc83f36 Merge pull request #1322 from drudge/ui/compose-monitoring-status
style(monitoring): use status badges for compose monitoring
2025-02-15 13:30:07 -06:00
Mauricio Siu
ba54124a56 Merge pull request #1321 from drudge/templates/outline
feat(template): add outline
2025-02-15 13:29:18 -06:00
Mauricio Siu
64bdf07dbd Merge pull request #1319 from mafrasil/feat-add-convex-tpl
feat(template): add convex.dev
2025-02-15 13:19:15 -06:00
Mauricio Siu
8366219266 refactor: format 2025-02-15 13:18:54 -06:00
Mauricio Siu
f38fb96eaf Merge branch 'canary' into feat-add-convex-tpl 2025-02-15 13:17:16 -06:00
Nicholas Penree
3b1ade804f style(monitoring): use status badges for compose monitoring 2025-02-15 14:16:30 -05:00
Mauricio Siu
fd69b45e5e refactor: update envs 2025-02-15 13:15:32 -06:00
Mauricio Siu
12034e460e Merge pull request #1316 from mezotv/patch-2
feat(template): update plausible
2025-02-15 13:08:23 -06:00
Mauricio Siu
2b5a1d90b0 Merge pull request #1309 from jeffersoncbd/canary
feat: Add Trilium app template
2025-02-15 13:07:38 -06:00
Mauricio Siu
e7195c8acf Update apps/dokploy/templates/trilium/docker-compose.yml 2025-02-15 13:07:33 -06:00
Nicholas Penree
9ace0f38cd chore(lint): fix lint 2025-02-15 02:22:51 -05:00
Nicholas Penree
796e50ed5f feat(template): add outline 2025-02-15 02:22:37 -05:00
mafrasil
ed54df9bd2 feat(template): add convex.dev 2025-02-14 09:23:41 +04:00
vicke4
a4242d45d3 style: commitlint fix 2025-02-14 10:05:19 +05:30
vicke4
121755d9cc refactor: add field description to message thread id 2025-02-14 10:04:38 +05:30
Dominik Koch
bf6c2698d4 fix: update plausible version 2025-02-13 21:52:01 +01:00
Dominik Koch
bbdda014d8 feat(template): update plausible 2025-02-13 21:46:06 +01:00
vicke4
316dfa6b2e style: consistent formatting of code 2025-02-13 22:27:39 +05:30
vicke4
e228325e37 feat: notifications to specific Telegram topics 2025-02-13 22:20:52 +05:30
Khiet Tam Nguyen
d9c83b7010 docs(template): note on networking for superset 2025-02-13 20:36:03 +11:00
Mauricio Siu
209029214e Merge pull request #1307 from vytenisstaugaitis/canary
chore: update wordpress version to v6.7.1
2025-02-12 22:16:14 -06:00
Jefferson Carlos
9de3bf3c32 Add files via upload 2025-02-13 00:33:11 -03:00
Jefferson Carlos
2c65fc22b0 Update templates.ts 2025-02-13 00:32:45 -03:00
Jefferson Carlos
6688b14753 Create docker-compose.yml 2025-02-13 00:22:18 -03:00
Jefferson Carlos
6ea2a82bb0 Create index.ts for Trilium app 2025-02-13 00:21:23 -03:00
vytenisstaugaitis
e01347673e chore: update wordpress version to v6.7.1 2025-02-12 22:31:52 +02:00
Mauricio Siu
bda9b05134 refactor: add ts ignore 2025-01-18 23:54:39 -06:00
Mauricio Siu
e7329a727f refactor: use stepper 2025-01-18 23:49:47 -06:00
Mauricio Siu
e68465f9e6 refactor: improve error 2025-01-18 23:07:36 -06:00
Mauricio Siu
5e7d344110 feat: add missing functions 2025-01-18 22:58:27 -06:00
Mauricio Siu
87546b4558 feat: add domains 2025-01-18 22:55:35 -06:00
Mauricio Siu
08ab18eebf refactor: add many AI providers & improve prompt 2025-01-18 21:35:03 -06:00
Mauricio Siu
ad642ab4e0 refactor: update migrations 2025-01-18 18:29:28 -06:00
Mauricio Siu
d5d8064b38 Merge branch 'canary' into kucherenko/canary 2025-01-18 18:29:12 -06:00
Andrey Kucherenko
d98fc82fbf fix: code review issues 2025-01-14 08:34:56 +01:00
Andrey Kucherenko
b58b6636e3 feat: add AI assistant to dokploy 2025-01-10 08:18:43 +01:00
405 changed files with 11907 additions and 32739 deletions

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Create a bug report
labels: ["bug"]
labels: ["needs-triage🔍"]
body:
- type: markdown
attributes:
@@ -62,6 +62,7 @@ body:
- "Docker"
- "Remote server"
- "Local Development"
- "Cloud Version"
validations:
required: true
- type: dropdown

BIN
.github/sponsors/synexa.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

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

View File

@@ -138,11 +138,18 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& ./install.sh
```
```bash
# Install Railpack
curl -sSL https://railpack.com/install.sh | sh
```
```bash
# Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
```
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.

View File

@@ -55,6 +55,10 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& ./install.sh \
&& pnpm install -g tsx
# Install Railpack
ARG RAILPACK_VERSION=0.0.37
RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack

View File

@@ -74,6 +74,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
</a>
</div>
### Premium Supporters 🥇
@@ -94,8 +96,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div>
### Community Backers 🤝
<div style="display: flex; gap: 30px; flex-wrap: wrap;">

View File

@@ -28,7 +28,7 @@ app.use(async (c, next) => {
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
const data = c.req.valid("json");
const res = queue.add(data, { groupName: data.serverId });
queue.add(data, { groupName: data.serverId });
return c.json(
{
message: "Deployment Added",

View File

@@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => {
}
}
}
} catch (error) {
} catch (_) {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") {

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -293,29 +293,6 @@ networks:
dokploy-network:
`;
const expectedComposeFile7 = `
version: "3.8"
services:
web:
image: nginx:latest
networks:
- dokploy-network
networks:
dokploy-network:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1200
backend:
driver: bridge
attachable: true
external_network:
external: true
name: dokploy-network
`;
test("It shoudn't add suffix to dokploy-network", () => {
const composeData = load(composeFile7) as ComposeSpecification;

View File

@@ -1,7 +1,7 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { dump, load } from "js-yaml";
import { load } from "js-yaml";
import { expect, test } from "vitest";
test("Generate random hash with 8 characters", () => {

View File

@@ -1,8 +1,4 @@
import { generateRandomHash } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesInServices,
} from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";

View File

@@ -51,20 +51,9 @@ const baseAdmin: User = {
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
accessedProjects: [],
accessedServices: [],
banExpires: new Date(),
banned: true,
banReason: "",
canAccessToAPI: false,
canCreateProjects: false,
canDeleteProjects: false,
canDeleteServices: false,
canAccessToDocker: false,
canAccessToSSHKeys: false,
canCreateServices: false,
canAccessToTraefikFiles: false,
canAccessToGitProviders: false,
email: "",
expirationDate: "",
id: "",
@@ -73,7 +62,6 @@ const baseAdmin: User = {
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
token: "",
updatedAt: new Date(),
twoFactorEnabled: false,
};
@@ -126,8 +114,6 @@ test("Should not touch config without host", () => {
});
test("Should remove websecure if https rollback to http", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(
{ ...baseAdmin, certificateType: "letsencrypt" },
"example.com",

View File

@@ -1,132 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { CardTitle } from "@/components/ui/card";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { AlertTriangle } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Login2FASchema = z.object({
pin: z.string().min(6, {
message: "Pin is required",
}),
});
type Login2FA = z.infer<typeof Login2FASchema>;
interface Props {
authId: string;
}
export const Login2FA = ({ authId }: Props) => {
const { push } = useRouter();
const { mutateAsync, isLoading, isError, error } =
api.auth.verifyLogin2FA.useMutation();
const form = useForm<Login2FA>({
defaultValues: {
pin: "",
},
resolver: zodResolver(Login2FASchema),
});
useEffect(() => {
form.reset({
pin: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: Login2FA) => {
await mutateAsync({
pin: data.pin,
id: authId,
})
.then(() => {
toast.success("Signin successfully", {
duration: 2000,
});
push("/dashboard/projects");
})
.catch(() => {
toast.error("Signin failed", {
duration: 2000,
});
});
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col max-sm:items-center">
<FormLabel>Pin</FormLabel>
<FormControl>
<div className="flex">
<InputOTP
maxLength={6}
{...field}
pattern={REGEXP_ONLY_DIGITS}
>
<InputOTPGroup>
<InputOTPSlot index={0} className="border-border" />
<InputOTPSlot index={1} className="border-border" />
<InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
</div>
</FormControl>
<FormDescription>
Please enter the 6 digits code provided by your authenticator
app.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={isLoading} type="submit">
Submit 2FA
</Button>
</form>
</Form>
);
};

View File

@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
}
try {
return JSON.parse(str);
} catch (e) {
} catch (_e) {
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
return z.NEVER;
}

View File

@@ -29,7 +29,6 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import React from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -17,7 +17,6 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -10,7 +10,6 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Rss, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { HandlePorts } from "./handle-ports";
interface Props {

View File

@@ -9,7 +9,6 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Split, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { HandleRedirect } from "./handle-redirect";

View File

@@ -9,7 +9,6 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { HandleSecurity } from "./handle-security";

View File

@@ -25,7 +25,7 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -8,7 +8,6 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { File, Loader2 } from "lucide-react";
import React from "react";
import { UpdateTraefikConfig } from "./update-traefik-config";
interface Props {
applicationId: string;

View File

@@ -10,7 +10,6 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes";

View File

@@ -21,7 +21,7 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -77,7 +77,7 @@ export const UpdateVolume = ({
serviceType,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const _utils = api.useUtils();
const { data } = api.mounts.one.useQuery(
{
mountId,

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -25,6 +26,7 @@ enum BuildType {
paketo_buildpacks = "paketo_buildpacks",
nixpacks = "nixpacks",
static = "static",
railpack = "railpack",
}
const mySchema = z.discriminatedUnion("buildType", [
@@ -53,6 +55,9 @@ const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal("static"),
}),
z.object({
buildType: z.literal("railpack"),
}),
]);
type AddTemplate = z.infer<typeof mySchema>;
@@ -173,6 +178,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
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" />

View File

@@ -11,7 +11,6 @@ import {
} from "@/components/ui/alert-dialog";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {

View File

@@ -73,15 +73,14 @@ export const ShowDeployments = ({ applicationId }: Props) => {
</div>
) : (
<div className="flex flex-col gap-4">
{deployments?.map((deployment) => (
{deployments?.map((deployment, index) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.status}
{index + 1}. {deployment.status}
<StatusTooltip
status={deployment?.status}
className="size-2.5"

View File

@@ -18,7 +18,7 @@ import { Toggle } from "@/components/ui/toggle";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { type CSSProperties, useEffect, useState } from "react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Card } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";

View File

@@ -84,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
data: repositories,
isLoading: isLoadingRepositories,
error,
isError,
} = api.bitbucket.getBitbucketRepositories.useQuery(
{
bitbucketId,

View File

@@ -11,7 +11,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
import { GitBranch, UploadCloud } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { SaveBitbucketProvider } from "./save-bitbucket-provider";

View File

@@ -7,7 +7,6 @@ import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import React from "react";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props {
@@ -28,8 +27,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
const { mutateAsync: stop, isLoading: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy, isLoading: isDeploying } =
api.application.deploy.useMutation();
const { mutateAsync: deploy } = api.application.deploy.useMutation();
const { mutateAsync: reload, isLoading: isReloading } =
api.application.reload.useMutation();

View File

@@ -5,7 +5,6 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,

View File

@@ -22,7 +22,6 @@ import {
RocketIcon,
Trash2,
} from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { AddPreviewDomain } from "./add-preview-domain";

View File

@@ -279,7 +279,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<FormField
control={form.control}
name="env"
render={({ field }) => (
render={() => (
<FormItem>
<FormControl>
<Secrets

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 { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -19,7 +19,6 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -60,7 +60,7 @@ export const DeleteService = ({ id, type }: Props) => {
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
const { data } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });

View File

@@ -11,7 +11,6 @@ import {
} from "@/components/ui/alert-dialog";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {

View File

@@ -118,7 +118,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
await deleteDomain({
domainId: item.domainId,
})
.then((data) => {
.then((_data) => {
refetch();
toast.success("Domain deleted successfully");
})

View File

@@ -35,8 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
const { mutateAsync, isLoading, error, isError } =
api.compose.update.useMutation();
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const form = useForm<AddComposeFile>({
defaultValues: {
@@ -76,7 +75,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
composeId,
});
})
.catch((e) => {
.catch((_e) => {
toast.error("Error updating the Compose config");
});
};

View File

@@ -84,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
data: repositories,
isLoading: isLoadingRepositories,
error,
isError,
} = api.bitbucket.getBitbucketRepositories.useQuery(
{
bitbucketId,

View File

@@ -7,7 +7,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { CodeIcon, GitBranch, LockIcon } from "lucide-react";
import { CodeIcon, GitBranch } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ComposeFileEditor } from "../compose-file-editor";

View File

@@ -70,7 +70,7 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
composeId,
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (data) => {
.then(async (_data) => {
randomizeCompose();
refetch();
toast.success("Compose updated");

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Dices } from "lucide-react";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -39,7 +39,7 @@ type Schema = z.infer<typeof schema>;
export const RandomizeCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
const [compose, setCompose] = useState<string>("");
const [isOpen, setIsOpen] = useState(false);
const [_isOpen, _setIsOpen] = useState(false);
const { mutateAsync, error, isError } =
api.compose.randomizeCompose.useMutation();
@@ -76,7 +76,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
suffix: formData?.suffix || "",
randomize: formData?.randomize || false,
})
.then(async (data) => {
.then(async (_data) => {
randomizeCompose();
refetch();
toast.success("Compose updated");

View File

@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
.then(() => {
refetch();
})
.catch((err) => {});
.catch((_err) => {});
}
}, [isOpen]);

View File

@@ -7,7 +7,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import React from "react";
import { ComposeActions } from "./actions";
import { ShowProviderFormCompose } from "./generic/show";
interface Props {

View File

@@ -18,7 +18,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(

View File

@@ -16,7 +16,6 @@ import {
import { api } from "@/utils/api";
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
import Link from "next/link";
import React from "react";
import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup";

View File

@@ -35,7 +35,7 @@ import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react";
import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -15,7 +15,6 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { CheckIcon } from "lucide-react";
import React from "react";
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";

View File

@@ -9,7 +9,6 @@ import {
import { cn } from "@/lib/utils";
import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash";
import React from "react";
import { type LogLine, getLogType } from "./utils";
interface LogLineProps {
@@ -48,23 +47,12 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
}
const htmlContent = fancyAnsi.toHtml(text);
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
const modifiedContent = htmlContent.replace(
/<span([^>]*)>([^<]*)<\/span>/g,
(match, attrs, content) => {
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
if (!content.match(searchRegex)) return match;
const segments = content.split(searchRegex);
const wrappedSegments = segments
.map((segment: string) =>
segment.toLowerCase() === term.toLowerCase()
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
: segment,
)
.join("");
return `<span${attrs}>${wrappedSegments}</span>`;
},
searchRegex,
(match) =>
`<span class="bg-orange-200/80 dark:bg-orange-900/80 font-bold">${match}</span>`,
);
return (

View File

@@ -1,6 +1,5 @@
import type { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {

View File

@@ -1,18 +1,3 @@
import {
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown, Container } from "lucide-react";
import * as React from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -37,6 +22,19 @@ import {
TableRow,
} from "@/components/ui/table";
import { type RouterOutputs, api } from "@/utils/api";
import {
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown, Container } from "lucide-react";
import * as React from "react";
import { columns } from "./colums";
export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]

View File

@@ -7,9 +7,8 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Tree } from "@/components/ui/file-tree";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react";
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
import React from "react";
import { ShowTraefikFile } from "./show-traefik-file";

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";

View File

@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
mariadbId: string;

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 { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";

View File

@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
mongoId: string;

View File

@@ -1,6 +1,7 @@
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
@@ -96,7 +97,10 @@ export const ComposeFreeMonitoring = ({
key={container.containerId}
value={container.name}
>
{container.name} ({container.containerId}) {container.state}
{container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>

View File

@@ -1,13 +1,7 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { api } from "@/utils/api";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { DockerBlockChart } from "./docker-block-chart";
import { DockerCpuChart } from "./docker-cpu-chart";
import { DockerDiskChart } from "./docker-disk-chart";
@@ -206,7 +200,7 @@ export const ContainerFreeMonitoring = ({
}, [appName]);
return (
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
<div className="rounded-xl bg-background flex flex-col gap-4">
<header className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>

View File

@@ -29,14 +29,6 @@ interface Props {
data: ContainerMetric[];
}
interface FormattedMetric {
timestamp: string;
read: number;
write: number;
readUnit: string;
writeUnit: string;
}
const chartConfig = {
read: {
label: "Read",

View File

@@ -1,3 +1,5 @@
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -102,7 +104,9 @@ export const ComposePaidMonitoring = ({
value={container.name}
>
{container.name} ({container.containerId}){" "}
{container.state}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>

View File

@@ -7,7 +7,6 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
import { CPUChart } from "./cpu-chart";
import { DiskChart } from "./disk-chart";
@@ -73,7 +72,7 @@ export const ShowPaidMonitoring = ({
data,
isLoading,
error: queryError,
} = api.user.getServerMetrics.useQuery(
} = api.server.getServerMetrics.useQuery(
{
url: BASE_URL,
token,

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";

View File

@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
mysqlId: string;

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 { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -9,18 +9,39 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { PenBoxIcon, Plus, SquarePen } from "lucide-react";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const organizationSchema = z.object({
name: z.string().min(1, {
message: "Organization name is required",
}),
logo: z.string().optional(),
});
type OrganizationFormValues = z.infer<typeof organizationSchema>;
interface Props {
organizationId?: string;
children?: React.ReactNode;
}
export function AddOrganization({ organizationId, children }: Props) {
export function AddOrganization({ organizationId }: Props) {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { data: organization } = api.organization.one.useQuery(
{
@@ -33,22 +54,37 @@ export function AddOrganization({ organizationId, children }: Props) {
const { mutateAsync, isLoading } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const form = useForm<OrganizationFormValues>({
resolver: zodResolver(organizationSchema),
defaultValues: {
name: "",
logo: "",
},
});
useEffect(() => {
if (organization) {
setName(organization.name);
form.reset({
name: organization.name,
logo: organization.logo || "",
});
}
}, [organization]);
const handleSubmit = async () => {
await mutateAsync({ name, organizationId: organizationId ?? "" })
}, [organization, form]);
const onSubmit = async (values: OrganizationFormValues) => {
await mutateAsync({
name: values.name,
logo: values.logo,
organizationId: organizationId ?? "",
})
.then(() => {
setOpen(false);
form.reset();
toast.success(
`Organization ${organizationId ? "updated" : "created"} successfully`,
);
utils.organization.all.invalidate();
setOpen(false);
})
.catch((error) => {
console.error(error);
@@ -57,6 +93,7 @@ export function AddOrganization({ organizationId, children }: Props) {
);
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -65,14 +102,11 @@ export function AddOrganization({ organizationId, children }: Props) {
className="group cursor-pointer hover:bg-blue-500/10"
onSelect={(e) => e.preventDefault()}
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="gap-2 p-2"
onClick={() => {
setOpen(true);
}}
onSelect={(e) => e.preventDefault()}
>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
@@ -91,28 +125,57 @@ export function AddOrganization({ organizationId, children }: Props) {
</DialogTitle>
<DialogDescription>
{organizationId
? "Update the organization name"
? "Update the organization name and logo"
: "Create a new organization to manage your projects."}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="col-span-3"
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4 py-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="tems-center gap-4">
<FormLabel className="text-right">Name</FormLabel>
<FormControl>
<Input
placeholder="Organization name"
{...field}
className="col-span-3"
/>
</FormControl>
<FormMessage className="" />
</FormItem>
)}
/>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
{organizationId ? "Update organization" : "Create organization"}
</Button>
</DialogFooter>
<FormField
control={form.control}
name="logo"
render={({ field }) => (
<FormItem className=" gap-4">
<FormLabel className="text-right">Logo URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/logo.png"
{...field}
value={field.value || ""}
className="col-span-3"
/>
</FormControl>
<FormMessage className="col-span-3 col-start-2" />
</FormItem>
)}
/>
<DialogFooter className="mt-4">
<Button type="submit" isLoading={isLoading}>
{organizationId ? "Update organization" : "Create organization"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);

View File

@@ -11,7 +11,7 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -53,7 +53,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
const { mutateAsync } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";

View File

@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
postgresId: string;

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 { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -0,0 +1,10 @@
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
interface Props {
projectId: string;
projectName?: string;
}
export const AddAiAssistant = ({ projectId }: Props) => {
return <TemplateGenerator projectId={projectId} />;
};

View File

@@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
projectId,
});
})
.catch((e) => {
.catch((_e) => {
toast.error("Error creating the service");
});
};

View File

@@ -18,7 +18,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -49,7 +48,6 @@ import { z } from "zod";
type DbType = typeof mySchema._type.type;
// TODO: Change to a real docker images
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
mongo: "mongo:6",
mariadb: "mariadb:11",

View File

@@ -57,7 +57,6 @@ import {
BookText,
CheckIcon,
ChevronsUpDown,
Code,
Github,
Globe,
HelpCircle,
@@ -384,9 +383,8 @@ export const AddTemplate = ({ projectId }: Props) => {
side="top"
>
<span>
If ot server is selected, the application
will be deployed on the server where the
user is logged in.
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
@@ -435,14 +433,14 @@ export const AddTemplate = ({ projectId }: Props) => {
});
toast.promise(promise, {
loading: "Setting up...",
success: (data) => {
success: (_data) => {
utils.project.one.invalidate({
projectId,
});
setOpen(false);
return `${template.name} template created successfully`;
},
error: (err) => {
error: (_err) => {
return `An error ocurred deploying ${template.name} template`;
},
});

View File

@@ -0,0 +1,102 @@
"use client";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} 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",
"Add a photo studio portfolio",
"Create a personal ad blocker",
"Build a social media dashboard",
"Sendgrid service opensource analogue",
];
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();
const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example });
};
return (
<div className="flex flex-col h-full gap-4">
<div className="">
<div className="space-y-4 ">
<h2 className="text-lg font-semibold">Step 1: Describe Your Needs</h2>
<div className="space-y-2">
<Label htmlFor="user-needs">Describe your template needs</Label>
<Textarea
id="user-needs"
placeholder="Describe the type of template you need, its purpose, and any specific features you'd like to include."
value={templateInfo?.userInput}
onChange={(e) =>
setTemplateInfo({ ...templateInfo, userInput: e.target.value })
}
className="min-h-[100px]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-deploy">
Select the server where you want to deploy (optional)
</Label>
<Select
value={templateInfo.server?.serverId}
onValueChange={(value) => {
const server = servers?.find((s) => s.serverId === value);
if (server) {
setTemplateInfo({
...templateInfo,
server: server,
});
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem key={server.serverId} value={server.serverId}>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Examples:</Label>
<div className="flex flex-wrap gap-2">
{examples.map((example, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => handleExampleClick(example)}
>
{example}
</Button>
))}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,109 @@
import { CodeEditor } from "@/components/shared/code-editor";
import ReactMarkdown from "react-markdown";
import type { StepProps } from "./step-two";
export const StepThree = ({ templateInfo }: StepProps) => {
return (
<div className="flex flex-col h-full">
<div className="flex-grow">
<div className="space-y-6">
<h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold">Name</h3>
<p className="text-sm text-muted-foreground">
{templateInfo?.details?.name}
</p>
</div>
<div>
<h3 className="text-sm font-semibold">Description</h3>
<ReactMarkdown className="text-sm text-muted-foreground">
{templateInfo?.details?.description}
</ReactMarkdown>
</div>
<div>
<h3 className="text-md font-semibold">Server</h3>
<p className="text-sm text-muted-foreground">
{templateInfo?.server?.name || "Dokploy Server"}
</p>
</div>
<div className="space-y-2">
<h3 className="text-sm font-semibold">Docker Compose</h3>
<CodeEditor
lineWrapping
value={templateInfo?.details?.dockerCompose}
disabled
className="font-mono"
/>
</div>
<div>
<h3 className="text-sm font-semibold">Environment Variables</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.envVariables.map(
(
env: {
name: string;
value: string;
},
index: number,
) => (
<li key={index}>
<strong className="text-sm font-semibold">
{env.name}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{env.value}
</span>
</li>
),
)}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold">Domains</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.domains.map(
(
domain: {
host: string;
port: number;
serviceName: string;
},
index: number,
) => (
<li key={index}>
<strong className="text-sm font-semibold">
{domain.host}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{domain.port} - {domain.serviceName}
</span>
</li>
),
)}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold">Configuration Files</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.configFiles.map((file, index) => (
<li key={index}>
<strong className="text-sm font-semibold">
{file.filePath}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{file.content}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,530 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { ScrollArea } from "@/components/ui/scroll-area";
import { api } from "@/utils/api";
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { toast } from "sonner";
import type { TemplateInfo } from "./template-generator";
export interface StepProps {
stepper?: any;
templateInfo: TemplateInfo;
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
}
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
const suggestions = templateInfo.suggestions || [];
const selectedVariant = templateInfo.details;
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
const { mutateAsync, isLoading, error, isError } =
api.ai.suggest.useMutation();
useEffect(() => {
if (suggestions?.length > 0) {
return;
}
mutateAsync({
aiId: templateInfo.aiId,
serverId: templateInfo.server?.serverId || "",
input: templateInfo.userInput,
})
.then((data) => {
setTemplateInfo({
...templateInfo,
suggestions: data,
});
})
.catch((error) => {
toast.error("Error generating suggestions", {
description: error.message,
});
});
}, [templateInfo.userInput]);
const toggleShowValue = (name: string) => {
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
};
const handleEnvVariableChange = (
index: number,
field: "name" | "value",
value: string,
) => {
if (!selectedVariant) return;
const updatedEnvVariables = [...selectedVariant.envVariables];
// @ts-ignore
updatedEnvVariables[index] = {
...updatedEnvVariables[index],
[field]: value,
};
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
envVariables: updatedEnvVariables,
},
}),
});
};
const removeEnvVariable = (index: number) => {
if (!selectedVariant) return;
const updatedEnvVariables = selectedVariant.envVariables.filter(
(_, i) => i !== index,
);
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
envVariables: updatedEnvVariables,
},
}),
});
};
const handleDomainChange = (
index: number,
field: "host" | "port" | "serviceName",
value: string | number,
) => {
if (!selectedVariant) return;
const updatedDomains = [...selectedVariant.domains];
// @ts-ignore
updatedDomains[index] = {
...updatedDomains[index],
[field]: value,
};
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
domains: updatedDomains,
},
}),
});
};
const removeDomain = (index: number) => {
if (!selectedVariant) return;
const updatedDomains = selectedVariant.domains.filter(
(_, i) => i !== index,
);
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
domains: updatedDomains,
},
}),
});
};
const addEnvVariable = () => {
if (!selectedVariant) return;
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
envVariables: [
...selectedVariant.envVariables,
{ name: "", value: "" },
],
},
}),
});
};
const addDomain = () => {
if (!selectedVariant) return;
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
domains: [
...selectedVariant.domains,
{ host: "", port: 80, serviceName: "" },
],
},
}),
});
};
if (isError) {
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<Bot className="w-16 h-16 text-primary animate-pulse" />
<h2 className="text-2xl font-semibold animate-pulse">Error</h2>
<AlertBlock type="error">
{error?.message || "Error generating suggestions"}
</AlertBlock>
</div>
);
}
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<Bot className="w-16 h-16 text-primary animate-pulse" />
<h2 className="text-2xl font-semibold animate-pulse">
AI is processing your request
</h2>
<p className="text-muted-foreground">
Generating template suggestions based on your input...
</p>
<pre>{templateInfo.userInput}</pre>
</div>
);
}
return (
<div className="flex flex-col h-full gap-6">
<div className="flex-grow overflow-auto pb-8">
<div className="space-y-6">
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
{!selectedVariant && (
<div className="space-y-4">
<div>Based on your input, we suggest the following variants:</div>
<RadioGroup
// value={selectedVariant?.}
onValueChange={(value) => {
const element = suggestions?.find((s) => s?.id === value);
setTemplateInfo({
...templateInfo,
details: element,
});
}}
className="space-y-4"
>
{suggestions?.map((suggestion) => (
<div
key={suggestion?.id}
className="flex items-start space-x-3"
>
<RadioGroupItem
value={suggestion?.id || ""}
id={suggestion?.id}
className="mt-1"
/>
<div>
<Label htmlFor={suggestion?.id} className="font-medium">
{suggestion?.name}
</Label>
<p className="text-sm text-muted-foreground">
{suggestion?.shortDescription}
</p>
</div>
</div>
))}
</RadioGroup>
</div>
)}
{selectedVariant && (
<>
<div className="mb-6">
<h3 className="text-xl font-bold">{selectedVariant?.name}</h3>
<p className="text-muted-foreground mt-2">
{selectedVariant?.shortDescription}
</p>
</div>
<ScrollArea>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="description">
<AccordionTrigger>Description</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border p-4">
<ReactMarkdown className="text-muted-foreground text-sm">
{selectedVariant?.description}
</ReactMarkdown>
</ScrollArea>
</AccordionContent>
</AccordionItem>
<AccordionItem value="docker-compose">
<AccordionTrigger>Docker Compose</AccordionTrigger>
<AccordionContent>
<CodeEditor
value={selectedVariant?.dockerCompose}
className="font-mono"
onChange={(value) => {
setTemplateInfo({
...templateInfo,
...(templateInfo?.details && {
details: {
...templateInfo.details,
dockerCompose: value,
},
}),
});
}}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value="env-variables">
<AccordionTrigger>Environment Variables</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.envVariables.map((env, index) => (
<div
key={index}
className="flex items-center space-x-2"
>
<Input
value={env.name}
onChange={(e) =>
handleEnvVariableChange(
index,
"name",
e.target.value,
)
}
placeholder="Variable Name"
className="flex-1"
/>
<div className="flex-1 relative">
<Input
type={
showValues[env.name] ? "text" : "password"
}
value={env.value}
onChange={(e) =>
handleEnvVariableChange(
index,
"value",
e.target.value,
)
}
placeholder="Variable Value"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 transform -translate-y-1/2"
onClick={() => toggleShowValue(env.name)}
>
{showValues[env.name] ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeEnvVariable(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="mt-2"
onClick={addEnvVariable}
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Variable
</Button>
</div>
</ScrollArea>
</AccordionContent>
</AccordionItem>
<AccordionItem value="domains">
<AccordionTrigger>Domains</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.domains.map((domain, index) => (
<div
key={index}
className="flex items-center space-x-2"
>
<Input
value={domain.host}
onChange={(e) =>
handleDomainChange(
index,
"host",
e.target.value,
)
}
placeholder="Domain Host"
className="flex-1"
/>
<Input
type="number"
value={domain.port}
onChange={(e) =>
handleDomainChange(
index,
"port",
Number.parseInt(e.target.value),
)
}
placeholder="Port"
className="w-24"
/>
<Input
value={domain.serviceName}
onChange={(e) =>
handleDomainChange(
index,
"serviceName",
e.target.value,
)
}
placeholder="Service Name"
className="flex-1"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeDomain(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="mt-2"
onClick={addDomain}
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Domain
</Button>
</div>
</ScrollArea>
</AccordionContent>
</AccordionItem>
<AccordionItem value="mounts">
<AccordionTrigger>Configuration Files</AccordionTrigger>
<AccordionContent>
<ScrollArea className="w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.configFiles?.length > 0 ? (
<>
<div className="text-sm text-muted-foreground mb-4">
This template requires the following
configuration files to be mounted:
</div>
{selectedVariant.configFiles.map(
(config, index) => (
<div
key={index}
className="space-y-2 border rounded-lg p-4"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label className="text-primary">
{config.filePath}
</Label>
<p className="text-xs text-muted-foreground">
Will be mounted as: ../files
{config.filePath}
</p>
</div>
</div>
<CodeEditor
value={config.content}
className="font-mono"
onChange={(value) => {
if (!selectedVariant?.configFiles)
return;
const updatedConfigFiles = [
...selectedVariant.configFiles,
];
updatedConfigFiles[index] = {
filePath: config.filePath,
content: value,
};
setTemplateInfo({
...templateInfo,
...(templateInfo.details && {
details: {
...templateInfo.details,
configFiles: updatedConfigFiles,
},
}),
});
}}
/>
</div>
),
)}
</>
) : (
<div className="text-center text-muted-foreground py-8">
<p>
This template doesn't require any configuration
files.
</p>
<p className="text-sm mt-2">
All necessary configurations are handled through
environment variables.
</p>
</div>
)}
</div>
</ScrollArea>
</AccordionContent>
</AccordionItem>
</Accordion>
</ScrollArea>
</>
)}
</div>
</div>
<div className="">
<div className="flex justify-between">
{selectedVariant && (
<Button
onClick={() => {
const { details, ...rest } = templateInfo;
setTemplateInfo(rest);
}}
variant="outline"
>
Change Variant
</Button>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,338 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { api } from "@/utils/api";
import { defineStepper } from "@stepperize/react";
import { Bot } from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
import { toast } from "sonner";
import { StepOne } from "./step-one";
import { StepThree } from "./step-three";
import { StepTwo } from "./step-two";
interface EnvVariable {
name: string;
value: string;
}
interface Domain {
host: string;
port: number;
serviceName: string;
}
interface Details {
name: string;
id: string;
description: string;
dockerCompose: string;
envVariables: EnvVariable[];
shortDescription: string;
domains: Domain[];
configFiles: Mount[];
}
interface Mount {
filePath: string;
content: string;
}
export interface TemplateInfo {
userInput: string;
details?: Details | null;
suggestions?: Details[];
server?: {
serverId: string;
name: string;
};
aiId: string;
}
const defaultTemplateInfo: TemplateInfo = {
aiId: "",
userInput: "",
server: undefined,
details: null,
suggestions: [],
};
export const { useStepper, steps, Scoped } = defineStepper(
{
id: "needs",
title: "Describe your needs",
},
{
id: "variant",
title: "Choose a Variant",
},
{
id: "review",
title: "Review and Finalize",
},
);
interface Props {
projectId: string;
projectName?: string;
}
export const TemplateGenerator = ({ projectId }: Props) => {
const [open, setOpen] = useState(false);
const stepper = useStepper();
const { data: aiSettings } = api.ai.getAll.useQuery();
const { mutateAsync } = api.ai.deploy.useMutation();
const [templateInfo, setTemplateInfo] =
useState<TemplateInfo>(defaultTemplateInfo);
const utils = api.useUtils();
const haveAtleasOneProviderEnabled = aiSettings?.some(
(ai) => ai.isEnabled === true,
);
const isDisabled = () => {
if (stepper.current.id === "needs") {
return !templateInfo.aiId || !templateInfo.userInput;
}
if (stepper.current.id === "variant") {
return !templateInfo?.details?.id;
}
return false;
};
const onSubmit = async () => {
await mutateAsync({
projectId,
id: templateInfo.details?.id || "",
name: templateInfo?.details?.name || "",
description: templateInfo?.details?.shortDescription || "",
dockerCompose: templateInfo?.details?.dockerCompose || "",
envVariables: (templateInfo?.details?.envVariables || [])
.map((env: any) => `${env.name}=${env.value}`)
.join("\n"),
domains: templateInfo?.details?.domains || [],
...(templateInfo.server?.serverId && {
serverId: templateInfo.server?.serverId || "",
}),
configFiles: templateInfo?.details?.configFiles || [],
})
.then(async () => {
toast.success("Compose Created");
setOpen(false);
await utils.project.one.invalidate({
projectId,
});
})
.catch(() => {
toast.error("Error creating the compose");
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<Bot className="size-4 text-muted-foreground" />
<span>AI Assistant</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl w-full flex flex-col">
<DialogHeader>
<DialogTitle>AI Assistant</DialogTitle>
<DialogDescription>
Create a custom template based on your needs
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<div className="flex justify-between">
<h2 className="text-lg font-semibold">Steps</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Step {stepper.current.index + 1} of {steps.length}
</span>
<div />
</div>
</div>
<Scoped>
<nav aria-label="Checkout Steps" className="group my-4">
<ol
className="flex items-center justify-between gap-2"
aria-orientation="horizontal"
>
{stepper.all.map((step, index, array) => (
<React.Fragment key={step.id}>
<li className="flex items-center gap-4 flex-shrink-0">
<Button
type="button"
role="tab"
variant={
index <= stepper.current.index ? "secondary" : "ghost"
}
aria-current={
stepper.current.id === step.id ? "step" : undefined
}
aria-posinset={index + 1}
aria-setsize={steps.length}
aria-selected={stepper.current.id === step.id}
className="flex size-10 items-center justify-center rounded-full border-2 border-border"
>
{index + 1}
</Button>
<span className="text-sm font-medium">{step.title}</span>
</li>
{index < array.length - 1 && (
<Separator
className={`flex-1 ${
index < stepper.current.index
? "bg-primary"
: "bg-muted"
}`}
/>
)}
</React.Fragment>
))}
</ol>
</nav>
{stepper.switch({
needs: () => (
<>
{!haveAtleasOneProviderEnabled && (
<AlertBlock type="warning">
<div className="flex flex-col w-full">
<span>AI features are not enabled</span>
<span>
To use AI-powered template generation, please{" "}
<Link
href="/dashboard/settings/ai"
className="font-medium underline underline-offset-4"
>
enable AI in your settings
</Link>
.
</span>
</div>
</AlertBlock>
)}
{haveAtleasOneProviderEnabled &&
aiSettings &&
aiSettings?.length > 0 && (
<div className="space-y-4">
<div className="flex flex-col gap-2">
<label
htmlFor="user-needs"
className="text-sm font-medium"
>
Select AI Provider
</label>
<Select
value={templateInfo.aiId}
onValueChange={(value) =>
setTemplateInfo((prev) => ({
...prev,
aiId: value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="Select an AI provider" />
</SelectTrigger>
<SelectContent>
{aiSettings.map((ai) => (
<SelectItem key={ai.aiId} value={ai.aiId}>
{ai.name} ({ai.model})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{templateInfo.aiId && (
<StepOne
setTemplateInfo={setTemplateInfo}
templateInfo={templateInfo}
/>
)}
</div>
)}
</>
),
variant: () => (
<StepTwo
templateInfo={templateInfo}
setTemplateInfo={setTemplateInfo}
/>
),
review: () => (
<StepThree
templateInfo={templateInfo}
setTemplateInfo={setTemplateInfo}
/>
),
})}
</Scoped>
</div>
<DialogFooter>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2 w-full justify-end">
<Button
onClick={stepper.prev}
disabled={stepper.isFirst}
variant="secondary"
>
Back
</Button>
<Button
disabled={isDisabled()}
onClick={async () => {
if (stepper.current.id === "needs") {
setTemplateInfo((prev) => ({
...prev,
suggestions: [],
details: null,
}));
}
if (stepper.isLast) {
await onSubmit();
return;
}
stepper.next();
// if (stepper.isLast) {
// // setIsOpen(false);
// // push("/dashboard/projects");
// } else {
// stepper.next();
// }
}}
>
{stepper.isLast ? "Create" : "Next"}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -21,7 +21,6 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, SquarePen } from "lucide-react";

View File

@@ -83,7 +83,7 @@ export const ShowProjects = () => {
</CardDescription>
</CardHeader>
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
{(auth?.role === "owner" || auth?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
@@ -286,7 +286,7 @@ export const ShowProjects = () => {
onClick={(e) => e.stopPropagation()}
>
{(auth?.role === "owner" ||
auth?.user?.canDeleteProjects) && (
auth?.canDeleteProjects) && (
<AlertDialog>
<AlertDialogTrigger className="w-full">
<DropdownMenuItem

View File

@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -79,7 +79,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
useEffect(() => {
const buildConnectionUrl = () => {
const hostname = window.location.hostname;
const _hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;

View File

@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";

View File

@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
redisId: string;

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 { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button";
import type { ColumnDef } from "@tanstack/react-table";
import { format } from "date-fns";
import { ArrowUpDown } from "lucide-react";
import * as React from "react";
import type { LogEntry } from "./show-requests";
export const getStatusColor = (status: number) => {
@@ -25,7 +24,7 @@ export const getStatusColor = (status: number) => {
export const columns: ColumnDef<LogEntry>[] = [
{
accessorKey: "level",
header: ({ column }) => {
header: () => {
return <Button variant="ghost">Level</Button>;
},
cell: ({ row }) => {

View File

@@ -92,7 +92,7 @@ export const RequestsTable = () => {
pageSize: 10,
});
const { data: statsLogs, isLoading } = api.settings.readStatsLogs.useQuery(
const { data: statsLogs } = api.settings.readStatsLogs.useQuery(
{
sort: sorting[0],
page: pagination,
@@ -300,7 +300,7 @@ export const RequestsTable = () => {
</div>
<Sheet
open={!!selectedRow}
onOpenChange={(open) => setSelectedRow(undefined)}
onOpenChange={(_open) => setSelectedRow(undefined)}
>
<SheetContent className="sm:max-w-[740px] flex flex-col">
<SheetHeader>

View File

@@ -11,7 +11,6 @@ import {
import { type RouterOutputs, api } from "@/utils/api";
import { ArrowDownUp } from "lucide-react";
import Link from "next/link";
import * as React from "react";
import { toast } from "sonner";
import { RequestDistributionChart } from "./request-distribution-chart";
import { RequestsTable } from "./requests-table";

View File

@@ -7,9 +7,7 @@ import {
PostgresqlIcon,
RedisIcon,
} from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
@@ -24,14 +22,11 @@ import {
extractServices,
} from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server/services/project";
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
import { useRouter } from "next/router";
import React from "react";
import { StatusTooltip } from "../shared/status-tooltip";
type Project = Awaited<ReturnType<typeof findProjectById>>;
export const SearchCommand = () => {
const router = useRouter();
const [open, setOpen] = React.useState(false);
@@ -40,7 +35,7 @@ export const SearchCommand = () => {
const { data } = api.project.all.useQuery(undefined, {
enabled: !!session,
});
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
React.useEffect(() => {
const down = (e: KeyboardEvent) => {

View File

@@ -0,0 +1,106 @@
"use client";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { BotIcon, Loader2, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleAi } from "./handle-ai";
export const AiForm = () => {
const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
const { mutateAsync, isLoading: isRemoving } = api.ai.delete.useMutation();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row gap-2 justify-between">
<div>
<CardTitle className="text-xl flex flex-row gap-2">
<BotIcon className="size-6 text-muted-foreground self-center" />
AI Settings
</CardTitle>
<CardDescription>Manage your AI configurations</CardDescription>
</div>
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{aiConfigs?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<BotIcon className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground text-center">
You don't have any AI configurations
</span>
<HandleAi />
</div>
) : (
<div className="flex flex-col gap-4 rounded-lg min-h-[25vh]">
{aiConfigs?.map((config) => (
<div
key={config.aiId}
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>
<span className="text-sm font-medium">
{config.name}
</span>
<CardDescription>{config.model}</CardDescription>
</div>
<div className="flex justify-between items-center">
<HandleAi aiId={config.aiId} />
<DialogAction
title="Delete AI"
description="Are you sure you want to delete this AI?"
type="destructive"
onClick={async () => {
await mutateAsync({
aiId: config.aiId,
})
.then(() => {
toast.success("AI deleted successfully");
refetch();
})
.catch(() => {
toast.error("Error deleting AI");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -0,0 +1,468 @@
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
prefix: z.string().optional(),
expiresIn: z.number().nullable(),
organizationId: z.string().min(1, "Organization is required"),
// Rate limiting fields
rateLimitEnabled: z.boolean().optional(),
rateLimitTimeWindow: z.number().nullable(),
rateLimitMax: z.number().nullable(),
// Request limiting fields
remaining: z.number().nullable().optional(),
refillAmount: z.number().nullable().optional(),
refillInterval: z.number().nullable().optional(),
});
type FormValues = z.infer<typeof formSchema>;
const EXPIRATION_OPTIONS = [
{ label: "Never", value: "0" },
{ label: "1 day", value: String(60 * 60 * 24) },
{ label: "7 days", value: String(60 * 60 * 24 * 7) },
{ label: "30 days", value: String(60 * 60 * 24 * 30) },
{ label: "90 days", value: String(60 * 60 * 24 * 90) },
{ label: "1 year", value: String(60 * 60 * 24 * 365) },
];
const TIME_WINDOW_OPTIONS = [
{ label: "1 minute", value: String(60 * 1000) },
{ label: "5 minutes", value: String(5 * 60 * 1000) },
{ label: "15 minutes", value: String(15 * 60 * 1000) },
{ label: "30 minutes", value: String(30 * 60 * 1000) },
{ label: "1 hour", value: String(60 * 60 * 1000) },
{ label: "1 day", value: String(24 * 60 * 60 * 1000) },
];
const REFILL_INTERVAL_OPTIONS = [
{ label: "1 hour", value: String(60 * 60 * 1000) },
{ label: "6 hours", value: String(6 * 60 * 60 * 1000) },
{ label: "12 hours", value: String(12 * 60 * 60 * 1000) },
{ label: "1 day", value: String(24 * 60 * 60 * 1000) },
{ label: "7 days", value: String(7 * 24 * 60 * 60 * 1000) },
{ label: "30 days", value: String(30 * 24 * 60 * 60 * 1000) },
];
export const AddApiKey = () => {
const [open, setOpen] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [newApiKey, setNewApiKey] = useState("");
const { refetch } = api.user.get.useQuery();
const { data: organizations } = api.organization.all.useQuery();
const createApiKey = api.user.createApiKey.useMutation({
onSuccess: (data) => {
if (!data) return;
setNewApiKey(data.key);
setOpen(false);
setShowSuccessModal(true);
form.reset();
void refetch();
},
onError: () => {
toast.error("Failed to generate API key");
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
prefix: "",
expiresIn: null,
organizationId: "",
rateLimitEnabled: false,
rateLimitTimeWindow: null,
rateLimitMax: null,
remaining: null,
refillAmount: null,
refillInterval: null,
},
});
const rateLimitEnabled = form.watch("rateLimitEnabled");
const onSubmit = async (values: FormValues) => {
createApiKey.mutate({
name: values.name,
expiresIn: values.expiresIn || undefined,
prefix: values.prefix || undefined,
metadata: {
organizationId: values.organizationId,
},
// Rate limiting
rateLimitEnabled: values.rateLimitEnabled,
rateLimitTimeWindow: values.rateLimitTimeWindow || undefined,
rateLimitMax: values.rateLimitMax || undefined,
// Request limiting
remaining: values.remaining || undefined,
refillAmount: values.refillAmount || undefined,
refillInterval: values.refillInterval || undefined,
});
};
return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Generate New Key</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Generate API Key</DialogTitle>
<DialogDescription>
Create a new API key for accessing the API. You can set an
expiration date and a custom prefix for better organization.
</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 API Key" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="prefix"
render={({ field }) => (
<FormItem>
<FormLabel>Prefix</FormLabel>
<FormControl>
<Input placeholder="my_app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiresIn"
render={({ field }) => (
<FormItem>
<FormLabel>Expiration</FormLabel>
<Select
value={field.value?.toString() || "0"}
onValueChange={(value) =>
field.onChange(Number.parseInt(value, 10))
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select expiration time" />
</SelectTrigger>
</FormControl>
<SelectContent>
{EXPIRATION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="organizationId"
render={({ field }) => (
<FormItem>
<FormLabel>Organization</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select organization" />
</SelectTrigger>
</FormControl>
<SelectContent>
{organizations?.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Rate Limiting Section */}
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-medium">Rate Limiting</h3>
<FormField
control={form.control}
name="rateLimitEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Enable Rate Limiting</FormLabel>
<FormDescription>
Limit the number of requests within a time window
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{rateLimitEnabled && (
<>
<FormField
control={form.control}
name="rateLimitTimeWindow"
render={({ field }) => (
<FormItem>
<FormLabel>Time Window</FormLabel>
<Select
value={field.value?.toString()}
onValueChange={(value) =>
field.onChange(Number.parseInt(value, 10))
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select time window" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TIME_WINDOW_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
The duration in which requests are counted
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="rateLimitMax"
render={({ field }) => (
<FormItem>
<FormLabel>Maximum Requests</FormLabel>
<FormControl>
<Input
type="number"
placeholder="100"
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number.parseInt(e.target.value, 10)
: null,
)
}
/>
</FormControl>
<FormDescription>
Maximum number of requests allowed within the time
window
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</div>
{/* Request Limiting Section */}
<div className="space-y-4 rounded-lg border p-4">
<h3 className="text-lg font-medium">Request Limiting</h3>
<FormField
control={form.control}
name="remaining"
render={({ field }) => (
<FormItem>
<FormLabel>Total Request Limit</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Leave empty for unlimited"
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number.parseInt(e.target.value, 10)
: null,
)
}
/>
</FormControl>
<FormDescription>
Total number of requests allowed (leave empty for
unlimited)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="refillAmount"
render={({ field }) => (
<FormItem>
<FormLabel>Refill Amount</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Amount to refill"
value={field.value?.toString() ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number.parseInt(e.target.value, 10)
: null,
)
}
/>
</FormControl>
<FormDescription>
Number of requests to add on each refill
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="refillInterval"
render={({ field }) => (
<FormItem>
<FormLabel>Refill Interval</FormLabel>
<Select
value={field.value?.toString()}
onValueChange={(value) =>
field.onChange(Number.parseInt(value, 10))
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select refill interval" />
</SelectTrigger>
</FormControl>
<SelectContent>
{REFILL_INTERVAL_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
How often to refill the request limit
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
Cancel
</Button>
<Button type="submit">Generate</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
<Dialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>API Key Generated Successfully</DialogTitle>
<DialogDescription>
Please copy your API key now. You won't be able to see it again!
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">
{newApiKey}
</div>
<div className="flex justify-end gap-3">
<Button
onClick={() => {
navigator.clipboard.writeText(newApiKey);
toast.success("API key copied to clipboard");
}}
>
Copy to Clipboard
</Button>
<Button
variant="outline"
onClick={() => setShowSuccessModal(false)}
>
Close
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -0,0 +1,142 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } 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();
const { mutateAsync: deleteApiKey, isLoading: isLoadingDelete } =
api.user.deleteApiKey.useMutation();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl flex items-center gap-2">
<KeyIcon className="size-5" />
API/CLI Keys
</CardTitle>
<CardDescription>
Generate and manage API keys to access the API/CLI
</CardDescription>
</div>
<div className="flex flex-row gap-2 max-sm:flex-wrap items-end">
<span className="text-sm font-medium text-muted-foreground">
Swagger API:
</span>
<Link
href="/swagger"
target="_blank"
className="flex flex-row gap-2 items-center"
>
<span className="text-sm font-medium">View</span>
<ExternalLinkIcon className="size-4" />
</Link>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex flex-col gap-4">
{data?.user.apiKeys && data.user.apiKeys.length > 0 ? (
data.user.apiKeys.map((apiKey) => (
<div
key={apiKey.id}
className="flex flex-col gap-2 p-4 border rounded-lg"
>
<div className="flex justify-between items-start">
<div className="flex flex-col gap-1">
<span className="font-medium">{apiKey.name}</span>
<div className="flex flex-wrap gap-2 items-center text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="size-3.5" />
Created{" "}
{formatDistanceToNow(new Date(apiKey.createdAt))}{" "}
ago
</span>
{apiKey.prefix && (
<Badge
variant="secondary"
className="flex items-center gap-1"
>
<Tag className="size-3.5" />
{apiKey.prefix}
</Badge>
)}
{apiKey.expiresAt && (
<Badge
variant="outline"
className="flex items-center gap-1"
>
<Clock className="size-3.5" />
Expires in{" "}
{formatDistanceToNow(
new Date(apiKey.expiresAt),
)}{" "}
</Badge>
)}
</div>
</div>
<DialogAction
title="Delete API Key"
description="Are you sure you want to delete this API key? This action cannot be undone."
type="destructive"
onClick={async () => {
try {
await deleteApiKey({
apiKeyId: apiKey.id,
});
await refetch();
toast.success("API key deleted successfully");
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Error deleting API key",
);
}
}}
>
<Button
variant="ghost"
size="icon"
isLoading={isLoadingDelete}
>
<Trash2 className="size-4" />
</Button>
</DialogAction>
</div>
</div>
))
) : (
<div className="flex flex-col items-center gap-3 py-6">
<KeyIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No API keys found
</span>
</div>
)}
</div>
{/* Generate new API key */}
<div className="flex justify-end pt-4 border-t">
<AddApiKey />
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -23,7 +23,7 @@ import {
PlusIcon,
} from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
import { useState } from "react";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
@@ -38,7 +38,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
return count * 3.5;
};
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
@@ -71,7 +71,7 @@ export const ShowBilling = () => {
});
const maxServers = admin?.user.serversQuantity ?? 1;
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
const percentage = ((servers ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
return (
@@ -102,13 +102,13 @@ export const ShowBilling = () => {
<div className="space-y-2 flex flex-col">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
You have {servers?.length} server on your plan of{" "}
You have {servers} server on your plan of{" "}
{admin?.user.serversQuantity} servers
</p>
<div>
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && admin.user.serversQuantity! <= servers?.length! && (
{admin && admin.user.serversQuantity! <= (servers ?? 0) && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">

View File

@@ -6,7 +6,6 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import type React from "react";
import { useEffect, useState } from "react";
export const ShowWelcomeDokploy = () => {

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