Compare commits

..

38 Commits

Author SHA1 Message Date
Mauricio Siu
a5bb9486a9 chore(package): bump version to v0.29.10 2026-07-06 03:37:58 -06:00
Mauricio Siu
c72698cc11 Merge pull request #4745 from Dokploy/feat/plan-limits
Feat/plan limits
2026-07-06 03:37:31 -06:00
Mauricio Siu
a1b26e8b83 Merge pull request #4744 from emi-ran/fix/sort-dropdown-scrollbar
fix(ui): prevent scrollbar layout shift
2026-07-06 03:37:16 -06:00
Mauricio Siu
b176f8f860 refactor(ui): streamline button styles and layout in dashboard components
- Removed unnecessary width class from buttons in ShowNodeApplications and ShowNodeConfig components for a cleaner design.
- Updated spacing in the NodeCard component to use gap instead of space-x for improved layout consistency.

These changes enhance the visual consistency and usability of the dashboard UI components.
2026-07-06 03:35:39 -06:00
autofix-ci[bot]
59b0e51ef7 [autofix.ci] apply automated fixes 2026-07-06 09:33:30 +00:00
Mauricio Siu
8c900408bd fix(tag-filter): update no tags message and improve layout
- Changed the message displayed when no tags are found to "No tags created yet." for better clarity.
- Added a consistent layout for the tag handling component in both TagFilter and TagSelector, ensuring a unified user experience.
2026-07-06 03:32:26 -06:00
Mauricio Siu
8db1250487 fix(ui): improve select component behavior and styling across various providers
- Added a check to prevent empty values from being processed in the onValueChange handler for Bitbucket, Gitea, GitHub, and GitLab providers.
- Removed unnecessary defaultValue prop from Select components to streamline the code.
- Updated button styles to remove background color for better consistency across the UI.
- Enhanced volume backup selection to display a message when no volumes are found.

This update enhances user experience by ensuring that empty selections are handled gracefully and improves the overall visual consistency of the UI components.
2026-07-06 03:27:52 -06:00
Emirhan
71bbbb44db fix(ui): prevent scrollbar layout shift 2026-07-06 10:39:16 +03:00
Mauricio Siu
2440e8f803 Merge branch 'main' into canary 2026-07-05 23:47:00 -06:00
VincentEmmanuel
ca2708a58a fix(ai): allow Ollama Cloud API key in AI settings (#4262)
The Ollama detection matched any URL containing "ollama", which hides
the API Key field for Ollama Cloud (ollama.com) and drops the key from
the createOllama() client, so cloud requests go out unauthenticated.

Narrow the rule to localhost-only Ollama and forward the API key as a
Bearer header when provided.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-05 17:03:08 -06:00
Rafael Dias Zendron
f8a3561f1e fix(backup): redact S3 credentials from logs and error output (#4648)
* fix(backup): redact S3 credentials from logs and error output (#4621)

S3 backup credentials (access key + secret) were logged in plaintext
to Dokploy service stdout via logger.info in getBackupCommand() and
console.error in keepLatestNBackups(). Any operator with access to
service logs could recover S3 credentials.

Added redactRcloneCredentials() pure function that masks
--s3-access-key-id and --s3-secret-access-key values with [REDACTED].
Applied to both the structured logger call and the error handler.

Closes #4621

* fix(backups): redact sensitive information in error logs during web server backup process

Updated error handling in the web server backup function to redact Rclone credentials from error messages before logging and notification. This change enhances security by preventing sensitive data exposure in logs.

---------

Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
2026-07-05 16:52:03 -06:00
Mauricio Siu
38de9ef218 fix(ai): allow configFiles to be null in template generator Details type (#4736) 2026-07-05 16:51:47 -06:00
Mauricio Siu
cb23d726fe feat(databases): add copy button to User and Database Name fields (#4735)
* feat(databases): add copy button to User and Database Name fields

Adds an enableCopyButton prop to the Input component and uses it on
the User, Database Name, and Internal Host fields across Postgres,
MySQL, MariaDB, MongoDB, Redis, and Libsql credential views.

Closes #4495

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-05 16:34:35 -06:00
Mauricio Siu
aa93897eae chore(package): bump dokploy version to v0.29.9 2026-07-05 16:31:48 -06:00
Guillaume Lecomte
475a01c4a2 fix(databases): update default Redis version from 7 to 8 (#4224)
Update Redis default image from redis:7 to redis:8 in:
- packages/server/src/setup/redis-setup.ts (internal Redis setup)
- packages/server/src/services/ai.ts (AI service template examples)
- apps/dokploy/components/dashboard/project/add-database.tsx (UI default)

Redis 8 provides better performance according to benchmarks while
maintaining stability. Closes #4172.
2026-07-05 16:21:51 -06:00
Mauricio Siu
8a0e44291f fix(deployment): resolve schedule to its service before permission check in allByType (#4733)
Members with access to a schedule's application/compose got a 401 on
deployment.allByType because it passed the scheduleId straight into
checkServicePermissionAndAccess, which expects a service id.
2026-07-05 16:17:27 -06:00
Mauricio Siu
3e11c0a240 fix(ai): use nullable instead of optional for configFiles in AI suggestion schema (#4732)
OpenAI strict mode for response_format requires every property to be
listed in 'required'. Optional fields must instead be modeled as
nullable so the key stays required while still allowing no value.

Fixes #4267
2026-07-05 15:38:05 -06:00
Mauricio Siu
b96c5e8655 fix(projects): make project cards grid fill available width on wide screens (#4731) 2026-07-05 15:06:54 -06:00
Mauricio Siu
3e74f9a374 fix(user): scope user.get relation columns to reduce SSR payload size (#4730)
apiKeys and nested user were returned without column projection, shipping
secrets (key, permissions, metadata) and unused fields to every page that
prefetches user.get, causing the /dashboard/home SSR payload to exceed
Next.js's 128kB warning threshold.
2026-07-05 15:01:21 -06:00
Mauricio Siu
b2692cd594 fix(domain): validate hostname format to reject invalid characters (#4729)
* fix(domain): validate hostname format to reject invalid characters

Underscores and other invalid characters were accepted in domain
inputs with no validation, causing Let's Encrypt to silently fail
certificate issuance while Dokploy fell back to a self-signed cert.

Fixes #4716

* fix(create-server): update SSH key label for clarity in server creation form
2026-07-05 14:53:01 -06:00
ioanbeilic
db0cb66f0d fix(server-setup): report the installed Docker version in the setup banner (#4723) 2026-07-05 14:31:03 -06:00
Mauricio Siu
91abc93c10 refactor(whitelabeling): update CSS variables to use oklch color format
Replaced existing color definitions in the whitelabeling settings with the oklch color format for improved color management and consistency. This change enhances the customization capabilities of the theme while maintaining compatibility with Tailwind CSS v4.

No functional changes were made to the application behavior.
2026-07-01 13:26:23 -06:00
agentHits
f5ded8b273 fix: use github owner login for webhook deploy matching (#4674)
* fix: use github owner login for webhook deploy matching

* fix: prefer github owner name for webhook matching

Что:
- Инвертирован порядок fallback для GitHub webhook owner: сначала repository.owner.name, затем repository.owner.login.
- Обновлен focused regression test для приоритета owner.name и fallback на owner.login.
Зачем:
- Выполнить maintainer review request в PR #4674 и сохранить совместимость deploy matching для payload без owner.name.
Риски:
- Не выявлены для push/tag matching; preview pull_request путь использует тот же helper, но отдельным PR-event тестом не покрыт.
Проверки:
- Команды и результаты: git diff --check -- apps/dokploy/pages/api/deploy/github.ts apps/dokploy/__test__/deploy/github-webhook-handler.test.ts - passed; CI=true corepack pnpm --dir apps/dokploy exec vitest --config __test__/vitest.config.ts run __test__/deploy/github-webhook-handler.test.ts --reporter=verbose - passed, 1 file / 4 tests; CI=true corepack pnpm exec biome check apps/dokploy/pages/api/deploy/github.ts apps/dokploy/__test__/deploy/github-webhook-handler.test.ts - exit 0, reported existing Number.parseInt radix info at github.ts:464; mandatory QA subagent Boyle - pass.
- Ограничения: repo-wide format-and-lint, typecheck, build, and test не запускались для этого точечного review fix.

What:
- Inverted the GitHub webhook owner fallback order to prefer repository.owner.name before repository.owner.login.
- Updated the focused regression test for owner.name precedence and owner.login fallback.
Why:
- Address the maintainer review request in PR #4674 while preserving deploy matching for payloads without owner.name.
Risks:
- None identified for push/tag matching; the preview pull_request path uses the same helper but is not covered by a dedicated PR-event test.
Checks:
- Commands and results: git diff --check -- apps/dokploy/pages/api/deploy/github.ts apps/dokploy/__test__/deploy/github-webhook-handler.test.ts - passed; CI=true corepack pnpm --dir apps/dokploy exec vitest --config __test__/vitest.config.ts run __test__/deploy/github-webhook-handler.test.ts --reporter=verbose - passed, 1 file / 4 tests; CI=true corepack pnpm exec biome check apps/dokploy/pages/api/deploy/github.ts apps/dokploy/__test__/deploy/github-webhook-handler.test.ts - exit 0, reported existing Number.parseInt radix info at github.ts:464; mandatory QA subagent Boyle - pass.
- Limitations: repo-wide format-and-lint, typecheck, build, and test were not run for this targeted review fix.
2026-07-01 13:23:24 -06:00
Mauricio Siu
d87229ccd3 fix: resolve traefik container dynamically in access-log cleanup (#4646)
The nightly access-log-cleanup job hardcoded "dokploy-traefik" as the
container name when sending SIGUSR1. In Docker Swarm mode Traefik runs as
a service task named "dokploy-traefik.1.<task-id>", so `docker exec
dokploy-traefik` fails every night with "No such container". The log file
is rotated (inode changes) but Traefik never reopens it, leaving the
on-disk access.log frozen while real logs go to a deleted file handle.

Resolve the running container id dynamically with `docker ps --filter`,
matching the pattern already used elsewhere in the codebase, so it works
for both standalone and swarm deployments. Skip gracefully if no running
container is found.

Closes #4620
2026-06-30 16:25:08 -06:00
Mauricio Siu
1bf661b621 fix: prevent request path truncation in request logs (#4643)
The RequestPath in the request log table was truncated to 82 characters
with an ellipsis when it exceeded 100 characters, hiding part of the
route. Show the full path and let it wrap with flex-wrap and break-all.

Fixes #4642
2026-06-30 16:19:52 -06:00
Mauricio Siu
6431e9b7b0 fix(validation): allow hashtag in git branch names (#4714)
Branch names containing '#' (e.g. feat#123) were rejected by
VALID_BRANCH_REGEX when saving a git provider configuration, even
though '#' is a legal git ref character.

Add '#' to the allowed character set. The change propagates to the
backend zod schemas and all provider UI forms, since they share this
constant.

'#' is not a shell injection vector: the regex still rejects every
character needed to terminate a command (; | & $ ( ) ` newline space
quotes), and '#' only starts a shell comment at the beginning of a
word, never mid-argument as in 'git clone --branch feat#123'.

Fixes #4585
2026-06-30 16:19:22 -06:00
weibeu
8d44c6a1e8 fix: don't let ssh-keyscan abort SSH git clones (#4605)
cloneGitRepository runs `ssh-keyscan <host> >> known_hosts` as one step
of a `set -e` script. Hosts whose SSH endpoint waits for the client's
identification string first — Hugging Face's hf.co among them — never
complete the keyscan handshake, so it exits 1 and `set -e` aborts the
deploy before `git clone` ever runs.

Make ssh-keyscan non-fatal and let the real ssh client record the host
key on first connect (StrictHostKeyChecking=accept-new), which reaches
hosts ssh-keyscan can't scan. Same TOFU trust model, so no regression;
GitHub/GitLab/Gitea still pre-seed and verify known_hosts as before.
2026-06-30 16:13:40 -06:00
Rafael Dias Zendron
ec9dd28924 fix(registry): preserve username case for ECR compatibility (#4632) (#4647)
Remove .toLowerCase() transform from registryUsernameSchema.
AWS ECR requires the username to be exactly 'AWS' (uppercase) for
authentication. Docker Hub usernames are case-insensitive for login,
so preserving case is safe for all providers.

Closes #4632
2026-06-30 16:07:15 -06:00
Divyansh Goyal
e32133d9a6 prevent social icons from overlapping in onboarding layout (#4692)
* prevent social icons from overlapping in onboarding layout

* fixed the alignment social icon forms

* revert

* fix: adjust spacing in social sign-in section of registration page

Updated the layout of the social sign-in options by adding a gap between the elements to improve visual clarity and user experience.

---------

Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
2026-06-30 16:00:46 -06:00
Mauricio Siu
aa72091316 fix: allow members with git providers permission to create and delete their own (#4713)
The canAccessToGitProviders legacy override only granted read access, so
members with the Git Providers toggle enabled could not add providers — the
create/delete endpoints require gitProviders.create / gitProviders.delete.
This mirrors how the SSH Keys toggle already grants read/create/delete.

The git-provider remove endpoint now restricts non owner/admin roles to
deleting only their own providers (matching the ownership model used for
visibility and sharing), while owner/admin can still delete any provider in
the organization.

Closes #4695
2026-06-30 15:57:50 -06:00
Mauricio Siu
c2a95870f5 feat: add claim mapping functionality to OIDC registration dialog (#4712)
- Introduced a new mapping schema to handle user claims for OIDC providers, allowing for customizable mapping of claims to user fields.
- Implemented default mapping logic for Azure and generic providers, ensuring appropriate claim handling based on the issuer.
- Enhanced the registration dialog UI to include fields for claim mapping, improving user experience and configurability.
- Updated parsing logic to accommodate claim mapping in OIDC configuration.

This update enhances the flexibility of OIDC integration by allowing users to define how claims from identity providers map to application user fields.
2026-06-30 15:49:05 -06:00
Mauricio Siu
3ca5afd49f Feat/tailwind v4 shadcn update (#4706)
* feat: update dependencies and enhance UI components in Dokploy

- Updated various package versions in pnpm-lock.yaml to improve compatibility and performance, including React and Tailwind CSS.
- Modified components.json to change the styling to "radix-nova" and added new properties for icon library and menu color.
- Refactored multiple components to use updated class names for better styling consistency and responsiveness.
- Removed unused Radix UI components from package.json to streamline dependencies.
- Adjusted layout and styling in several dashboard components for improved user experience and visual appeal.
- Enhanced tooltip and form item components for better accessibility and usability.

* refactor: enhance UI components in HandleCertificate and SidebarLogo

- Updated the HandleCertificate component to improve dialog content height and textarea styling for better usability.
- Adjusted the SidebarLogo component layout to enhance alignment and spacing, ensuring a more consistent appearance across the sidebar.
- Implemented responsive design adjustments to textarea elements, preventing overflow and improving user experience.

* refactor: improve UI consistency and styling across dashboard components

- Updated Card components in ShowDeployments, ShowSchedules, and ShowVolumeBackups to remove unnecessary border styles for a cleaner look.
- Enhanced TabsList components in ShowProviderForm and ShowProviderFormCompose by adding a variant for improved visual distinction.
- Adjusted spacing in AdvancedEnvironmentSelector for better layout and readability.
- Removed redundant border classes in Service component for a more streamlined design.

* [autofix.ci] apply automated fixes

* chore: update package dependencies and refactor email rendering

- Upgraded React and React DOM to version 19.2.7 across multiple packages for improved performance and compatibility.
- Updated TSX to version 4.22.4 in various package.json files.
- Refactored email rendering from `renderAsync` to `render` in notification utilities and email templates for consistency.
- Adjusted Tailwind configuration usage in email templates to utilize a centralized configuration file.

These changes enhance the overall stability and maintainability of the codebase.

* refactor: update badge variants across dashboard components

- Changed badge variant from "outline-solid" to "outline" in multiple components including columns, show-domains, and various deployment tables for consistency in styling.
- Updated button variants in billing and project templates to align with the new badge styling.
- Enhanced input components to support password generation and error messaging, improving user experience.

These changes streamline the UI and ensure a cohesive design across the application.

* refactor: clean up calendar component imports and structure

- Removed redundant imports of icons from lucide-react and streamlined the import statements for better readability.
- Adjusted the placement of type imports to enhance code organization.

These changes improve the maintainability and clarity of the calendar component.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-30 15:45:23 -06:00
viky
8b6481501e fix: add method="post" to auth forms to prevent credential leak in URL (#4683)
Auth forms (login, register, 2FA, backup-code, reset-password) had no
method attribute, defaulting to GET. react-hook-form's handleSubmit
preventDefault()s only after hydration; submitting in the pre-hydration
or no-JS window triggers a native GET to the current URL, leaking
email/password into the URL, history, access logs and Referer header.

Setting method="post" makes the native fallback a POST so credentials
go in the request body instead. Normal JS submit path is unchanged.
Verified in a browser: GET (?email&password) -> POST (clean URL).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:03:13 -06:00
Mauricio Siu
ed0abb2465 feat: enhance TLS certificate selection UI in AddDomain component (#4705)
* feat: enhance TLS certificate selection UI in AddDomain component

- Added descriptive FormDescriptions for different TLS certificate options (None, Let's Encrypt, Custom) to improve user understanding of certificate provisioning.
- Updated placeholder text for the custom certificate resolver input field to provide clearer guidance on expected input format.

This update enhances the user experience by providing contextual information directly within the form.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-29 12:01:26 -06:00
Elijah
b4e2d274b1 fix: resolve server from parent entity in deployment.readLogs (#4689)
The readLogs endpoint only checked deployment.serverId and
deployment.schedule?.serverId to determine where to read log files.
For application and compose deployments on remote servers, serverId
is not stored on the deployment record — the server is resolved from
the parent entity (application.serverId, compose.serverId).

This caused readLogs to fall through to local execAsync, which would
silently fail (2>/dev/null) because the log file lives on the remote
server, returning empty content.

Fix by loading the compose relation in findDeploymentById and adding
fallback resolution through application?.serverId and compose?.serverId.

Fixes #4687

Co-authored-by: Roo <roo@agent.com>
2026-06-29 11:10:19 -06:00
Mauricio Siu
24b02f5523 Feat/concurrent deployments in memory queue (#4645)
* feat: add builds concurrency management for servers

- Introduced a new `BuildsConcurrency` component to manage the number of concurrent builds for both local and remote servers, gated by license validity.
- Implemented backend logic to resolve effective builds concurrency based on server settings and organization licenses.
- Added unit tests for concurrency resolution logic to ensure correct behavior under various licensing scenarios.
- Updated database schema to include `buildsConcurrency` field for servers and web server settings.
- Refactored deployment queue to support in-memory job processing with configurable concurrency limits.

This feature enhances deployment flexibility and control for enterprise users.

* refactor: enhance deployment cancellation logic and improve Railpack build isolation

- Reintroduced the `initCancelDeployments` function in the server initialization sequence to ensure deployments can be canceled effectively.
- Updated the Railpack build command to use a unique builder name for each build, preventing conflicts during concurrent deployments.
- Enhanced the cancellation logic to reset application and compose statuses to "idle" after canceling running deployments, improving system reliability.

* test: add buildsConcurrency setting to server configuration tests

- Introduced a new `buildsConcurrency` property in the server configuration tests to ensure proper handling of concurrent builds in deployment scenarios.

* feat: implement builds concurrency management and validation

- Added `assertBuildsConcurrencyAllowed` function to validate concurrency settings based on license status.
- Updated `resolveBuildsConcurrency` to reflect new concurrency limits for free and enterprise tiers.
- Enhanced `BuildsConcurrency` component to manage concurrent builds for servers, with UI adjustments for better user experience.
- Introduced a new settings page for managing concurrent builds across servers, ensuring proper handling of deployments.
- Updated database schema to support increased maximum concurrency values for servers and web server settings.
2026-06-16 23:15:19 -06:00
github-actions[bot]
b3c2e1e5af 🚀 Release v0.29.8 (#4562)
* fix(migrate-auth-secret): exit cleanly when there are no 2FA records

The empty-records branch of `main()` returned without calling
`process.exit(0)`, leaving the Drizzle Postgres connection pool
holding the event loop open. The `migrate-auth-secret` process
then hangs indefinitely after printing "No 2FA records found,
nothing to migrate." causing the upstream `0.29.3.sh` security
migration script (which calls this via `docker exec`) to never
reach its final `docker service update` step that mounts the new
Docker Secret. Operators end up with the new secret created but
the dokploy service still configured with the hardcoded
`BETTER_AUTH_SECRET`, while believing the migration completed.

Match the success branch a few lines below which already does
`process.exit(0)`, and the pattern used in sibling scripts
`reset-password.ts` and `reset-2fa.ts`.

Closes #4392

* feat(compose): add import from base64 in create service dropdown

Adds an "Import" option to the Create Service dropdown that lets users
paste a base64-encoded compose export, preview the template (compose YAML,
domains, envs, mounts) before confirming, and create the service only on
confirm. Adds a `previewTemplate` tRPC procedure that processes the base64
without touching the DB, with server access validation via session.

* [autofix.ci] apply automated fixes

* Enhance version synchronization workflow to include SDK repository

- Updated the GitHub Actions workflow to sync versioning across MCP, CLI, and SDK repositories.
- Added steps to bump the version in the SDK repository and regenerate tools from the latest OpenAPI spec.
- Improved commit message formatting to include source and release information for all repositories.
- Ensured successful synchronization messages for each repository after the version update.

* feat(deployment): add readLogs procedure to fetch deployment logs

- Introduced a new `readLogs` procedure that allows users to retrieve logs for a specific deployment by providing the deployment ID and an optional tail parameter.
- Implemented permission checks to ensure users have access to the requested logs.
- Enhanced log retrieval for both cloud and non-cloud environments, utilizing appropriate commands based on the server context.

Resolve https://github.com/Dokploy/mcp/issues/14

* feat(deployment): add server access validation for deployment actions

- Implemented server access validation in deployment procedures to ensure users can only access deployments associated with their active organization.
- Added checks to throw an UNAUTHORIZED error if a user attempts to access a deployment linked to a server outside their organization.

This enhancement improves security and access control within the deployment management system.

* feat(organization): prevent inviting users with owner role

- Added validation to prevent users from being invited with the owner role in the organization and user routers.
- Implemented TRPCError responses to ensure proper error handling when attempting to assign the owner role.
This change enhances role management and security within the organization structure.

https://github.com/Dokploy/dokploy/security/advisories/GHSA-fm9p-wmpw-gxjh

* feat(user): implement session cleanup on user update

- Added functionality to delete old sessions when a user updates their password, ensuring that only the current session remains active.
- This change enhances security by preventing unauthorized access from previous sessions after a password change.

Close here https://github.com/Dokploy/dokploy/security/advisories/GHSA-rr9m-w87g-46f3

* feat(settings): add copy button to server IP in web server settings (#4397)

* fix: copy Dokploy server IP when clicking server badge (#4390)

* fix: copy Dokploy server IP when clicking server badge

When a service runs on the local Dokploy server (no remote server),
clicking the server badge did nothing because `data.server` is null.
Now falls back to the server IP from settings so the badge always
copies an IP address.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(copy-ip): implement IP address copying functionality across database service components

- Added the ability to copy the server IP address to the clipboard when clicking the server badge in various database service components (Libsql, MariaDB, MongoDB, MySQL, PostgreSQL, Redis).
- Integrated the `copy-to-clipboard` library and `sonner` for user feedback upon successful copy action.
- Ensured fallback to the server IP from settings when the service data is not available, enhancing user experience and functionality.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* fix: responsive layout (#4391)

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>

* fix: automatically converting username to lowercase both in creation of register, and build for extra. (#4382)

* fix: allow square brackets in zip path validation for Next.js dynamic routes (#4468)

* fix: allow square brackets in zip drop path validation for Next.js dynamic routes

ZIP uploads containing Next.js dynamic route files (e.g. app/api/[id]/route.ts,
pages/[slug].tsx) were rejected by readValidDirectory because the path regex
did not include square bracket characters.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: prevent webhook deploy crash when commit data lacks modified files (#4470)

shouldDeploy passed undefined/null entries from commit.modified straight
into micromatch, which throws "Expected input to be a string" and fails
every webhook deployment when watch paths are configured. Filter out
non-string values before matching.

* fix: add type="button" to TooltipTrigger in form components to prevent accidental submission (#4422)

Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>

* fix: enable comment toggle shortcut in env variable editor (#4402) (#4473)

* fix: add tls=true label for domains when certificateType is none (#4018) (#4474)

* fix: add tls=true label for compose domains when certificateType is none (#4018)

* test: cover tls=true label for certificateType none, require https

* fix: scope tls fix to compose labels, leave traefik file config unchanged (#4018)

* chore: update version to v0.29.5 in package.json

* chore(deps): upgrade next to 16.2.6 (#4477)

Upgraded next dependency in apps/dokploy to 16.2.6 exactly. Verified typescript typecheck passes successfully.

* feat: add self-hosted enterprise restrictions (remote-servers-only, enforce-sso) (#4511)

* feat: add self-hosted enterprise restrictions (remote-servers-only, enforce-sso)

- Add `remoteServersOnly` field to webServerSettings: prevents creating services
  on the local Dokploy VM, forcing all deployments to remote servers. Validated
  in all 8 service routers (application, compose, postgres, mysql, mongo, redis,
  mariadb, libsql).
- Add `enforceSSO` field to webServerSettings: hides the email/password login
  form and shows only the SSO button on the login page.
- Both settings are enterprise-only (enterpriseProcedure) and self-hosted-only
  (blocked at the API level when IS_CLOUD=true).
- UI toggles added to the SSO settings page under a new "Self-hosted
  Restrictions" card (hidden in cloud). Login page reads enforceSSO from
  getServerSideProps to avoid client-side flash.
- Migrations: 0167_fresh_goliath.sql, 0168_long_justice.sql

* fix: add missing final newlines to migration files

* refactor: improve code formatting for better readability in multiple components

- Adjusted formatting in `add-application.tsx`, `add-compose.tsx`, and `add-database.tsx` to enhance readability by adding line breaks and consistent indentation.
- Updated `toggle-enforce-sso.tsx` to simplify the Switch component's props.
- Reformatted imports in `index.tsx` and `sso.tsx` for consistency.
- Cleaned up conditional statements in various router files for improved clarity.

* fix: add enforceSSO to test mock

* fix: grant create and delete SSH key permissions when canAccessToSSHKeys is enabled for members (#4512)

* fix: use create permission for basic auth delete instead of delete (#4513)

* fix: wrap long server names and keep actions menu visible (#4434)

On settings/servers, a long server name in the card title (h3) did not
wrap and overflowed its container, overlapping nearby content and
squeezing the three-dots actions menu until it disappeared.

Allow the title block to shrink and wrap (min-w-0 + break-words), keep
the server icon and the actions trigger from being crushed (shrink-0),
and add gap between the title and the actions button.

* chore: update version to v0.29.6 in package.json

* fix: preserve HOME in compose deploy so --with-registry-auth can read docker config (#4485)

The compose/stack deploy command runs under `env -i PATH="$PATH"`, which
clears the environment except for PATH. That strips HOME, so when the
generated command is `docker stack deploy --prune --with-registry-auth`
the docker CLI cannot resolve `~/.docker/config.json` (e.g.
`/root/.docker/config.json`) and ships no registry credentials to the
swarm. Private-registry images then fail to pull on the nodes:

  image registry.example.com/... could not be accessed on a registry to
  record its digest. Each node will access ... independently

while the deploy still logs "Docker Compose Deployed: ".

Keep PATH isolation but preserve HOME so docker can read its config for
both `stack deploy --with-registry-auth` and `compose up -d --build`.

Add a regression test asserting the generated command preserves
`HOME="$HOME"` for both stack and docker-compose deploys.

Fixes #4401

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: scope dokploy-server schedules to organization instead of user (#4526)

* fix: scope dokploy-server schedules to organization instead of user

Replaces userId with organizationId on the schedule table so that
global (dokploy-server) schedules are shared across all owners and
admins of the same organization, while remaining isolated between
different organizations.

Includes a data migration that backfills organizationId from the
owner membership record for any existing dokploy-server schedules.

Closes #4300

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* feat: implement forward authentication settings and UI components

- Added a new `forward_auth_settings` table to manage authentication domains and their configurations.
- Introduced UI components for handling forward authentication, including enabling/disabling SSO for domains and selecting SSO providers.
- Updated existing tests to include validation for the new `forwardAuthProviderId` field in domain configurations.
- Enhanced the dashboard to integrate forward authentication management, allowing users to configure SSO settings directly from the application interface.

This update improves the flexibility and security of application authentication by allowing integration with various identity providers.

* refactor: simplify forward authentication handling in UI and API

- Removed the selection of SSO providers from the UI, streamlining the process to enable/disable SSO for domains.
- Updated the API to eliminate the need for a provider ID when enabling forward authentication, relying on the configured settings instead.
- Enhanced user feedback by updating toast messages to reflect the current state of SSO authentication.
- Improved the UI layout for better clarity on SSO status and actions.

This refactor enhances the user experience by simplifying the SSO configuration process and ensuring clearer communication of actions taken.

* refactor: unify branch validation imports across provider components

- Added the `VALID_BRANCH_REGEX` import to all Git provider components to ensure consistent branch validation.
- Removed duplicate imports of `VALID_BRANCH_REGEX` to streamline the code and improve readability.

This change enhances maintainability by centralizing branch validation logic across the application.

* refactor: remove obsolete SQL migration files and snapshots

- Deleted several SQL migration files related to the `webServerSettings` and `schedule` tables, which included adding and dropping columns and constraints.
- Removed snapshots corresponding to the deleted migrations to maintain consistency in the database schema history.

This cleanup enhances the maintainability of the migration history by removing outdated and unused files.

* refactor: update forward authentication handling in domain schema and tests

- Replaced `forwardAuthProviderId` with `forwardAuthEnabled` in the domain schema to simplify the configuration of forward authentication.
- Updated related tests to reflect this change, ensuring consistency across the application.
- Introduced a new SQL migration to create the `forward_auth_settings` table for managing authentication domains and their configurations.

This refactor enhances the clarity and maintainability of the forward authentication logic within the application.

* chore: remove PR quality workflow configuration

Deleted the `.github/workflows/pr-quality.yml` file, which contained the configuration for the PR Quality workflow. This removal streamlines the repository by eliminating unused workflow files.

* Delete .github/workflows/pr-quality.yml

* refactor: enhance forward authentication UI and API integration

- Updated the alert block in the HandleForwardAuth component to provide clearer requirements for deploying the authentication proxy.
- Added a DnsHelperModal to assist with DNS configuration in the ForwardAuthServers component.
- Refined API input schemas for forward authentication operations to improve type safety and clarity.
- Removed the obsolete forward-auth SSO design document to streamline documentation.

These changes improve the user experience and maintainability of the forward authentication feature across the application.

* feat: add SQL migration for lucky echo and update foreign key constraints

- Introduced a new SQL migration file `0171_lucky_echo.sql` to modify the foreign key constraint on the `sso_provider` table, changing the `ON DELETE` behavior from `cascade` to `set null`.
- Updated the journal to include the new migration version and its associated tag.
- Added a snapshot file for version 7 of the database schema, reflecting the current state of the `sso_provider` and other related tables.

These changes enhance the integrity of the database by ensuring that user references are set to null instead of being deleted when the referenced user is removed.

* refactor: improve path validation in Traefik configuration schema

- Enhanced the `apiReadTraefikConfig` schema by reintroducing path validation logic to prevent directory traversal attacks and unauthorized access.
- The validation now includes checks for null bytes and ensures paths start with a defined main Traefik path, improving security and robustness.

These changes strengthen the integrity of the configuration handling by ensuring only valid paths are accepted.

* fix: swarm health check fields not resetting to default values (#4558)

Fixes #4553

- Replace z.coerce.number() with a custom transform that converts empty strings to undefined instead of 0
- Add value={field.value ?? ""} to numeric inputs so they visually clear when reset to undefined

* fix: add docker cleanup toggle to remote server creation (#4559)

* fix: add docker cleanup toggle to remote server creation and update forms

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: use stop-first update order for all database services (#4560)

Docker Swarm's default start-first update order causes new database
containers to fail with 'DBPathInUse' because two containers compete
for the same data volume simultaneously. Docker then rolls back the
update, silently reverting any env var or config changes.

Using stop-first ensures the old container is stopped before the new
one starts, preventing volume lock conflicts across all database types.

Fixes #4550

* fix: respect gitProviders permissions in git provider UI (#4561)

* chore: bump dokploy version to v0.29.8

* fix: strip credentials from service-level API responses (#4564)

* fix: strip credentials from service-level API responses

Registry passwords and S3 destination credentials were being returned
in service `.one` tRPC endpoints to any user with service-level read
access. Reported by Nihon Kohden Corporation security team.

- Strip registry `password` from `findApplicationById` via Drizzle `columns: { password: false }`
- Strip destination `accessKey`/`secretAccessKey` from all DB service finders (postgres, mysql, mariadb, mongo, libsql, compose, backup, volume-backups)
- Add `findRegistryByIdWithCredentials` for internal use only
- Builders and upload utils now load registry credentials by ID at execution time
- `createRollback` enriches `fullContext` with registry credentials before persisting to DB so rollback execution has what it needs
- Remove `findApplicationByIdWithCredentials` and `ApplicationNestedWithCredentials` — no longer needed
- Backup execution utils load full destination via `findDestinationById` at runtime instead of reading from the joined relation

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* refactor: improve restore logging for database backups (#4566)

* refactor: improve restore logging for database backups

- Updated restore functions across various database types (Postgres, MySQL, MongoDB, MariaDB, LibSQL, and Compose) to provide clearer logging messages.
- Replaced generic command execution logs with specific messages indicating the database being restored and the source backup file.
- This change enhances the clarity of restore operations and aids in troubleshooting by providing more context in the logs.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: use swarm advertise address in docker swarm join command (#4567)

* fix: enforce docker:read on container start/stop/kill/restart mutations (#4568)

* refactor: replace BETTER_AUTH_SECRET with betterAuthSecret in forward-auth setup

* fix: update deriveCookieSecret to meet oauth2-proxy requirements

* fix: correct deriveCookieSecret test to validate 16-byte hex secret as per oauth2-proxy requirements

* fix: strip credentials from gitProvider.getAll API response (#4569)

* fix: strip credentials from gitProvider.getAll API response

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: correct git provider access check for existing deploys (#4570)

* fix: use canEditDeployGitSource for git provider access on existing deploys

Replaces the simple userId ownership check with a new canEditDeployGitSource
function that correctly handles all role/sharing scenarios. Owner always has
access; admin and member only if they own the provider or it is shared with
the org — being assigned via accessedGitProviders (enterprise) only grants
permission to connect new deploys, not to edit the git source of existing ones.

Adds 26 unit tests covering owner, admin, member (with/without enterprise
license), shared providers, and the key regression case from issue #4469.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: prevent registry password from appearing in error messages and shell commands (#4579)

---------

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
Co-authored-by: ngenohkevin <ngenohkevin19@gmail.com>
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Volodymyr Kravchuk <volodymyr.kravch@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nahidujjaman Hridoy <75487507+nhridoy@users.noreply.github.com>
Co-authored-by: Francis <9560564+Baker@users.noreply.github.com>
Co-authored-by: mixelburg <52622705+mixelburg@users.noreply.github.com>
Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>
Co-authored-by: Jasael <67719321+jasael@users.noreply.github.com>
Co-authored-by: Philippe Parage <69145356+pparage@users.noreply.github.com>
Co-authored-by: youcef zr <93142224+youcefzemmar@users.noreply.github.com>
2026-06-08 09:20:55 -06:00
Mauricio Siu
60867d0b60 Merge pull request #4537 from Dokploy/canary
🚀 Release v0.29.7
2026-06-02 02:31:10 -06:00
287 changed files with 19317 additions and 6188 deletions

View File

@@ -17,17 +17,17 @@
"hono": "^4.11.7", "hono": "^4.11.7",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"react": "18.2.0", "react": "19.2.7",
"react-dom": "18.2.0", "react-dom": "19.2.7",
"redis": "4.7.0", "redis": "4.7.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.4.0", "@types/node": "^24.4.0",
"@types/react": "^18.2.37", "@types/react": "^19.2.0",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^19.2.0",
"rimraf": "6.1.3", "rimraf": "6.1.3",
"tsx": "^4.16.2", "tsx": "^4.22.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@10.22.0", "packageManager": "pnpm@10.22.0",

View File

@@ -0,0 +1,50 @@
import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact";
import { describe, expect, it } from "vitest";
describe("redactRcloneCredentials (#4621)", () => {
it("should redact access key in rclone command", () => {
const cmd =
'rclone rcat --s3-access-key-id="AKIAIOSFODNN7EXAMPLE" --s3-secret-access-key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" :s3:bucket/file.gz';
const redacted = redactRcloneCredentials(cmd);
expect(redacted).not.toContain("AKIAIOSFODNN7EXAMPLE");
expect(redacted).toContain('--s3-access-key-id="[REDACTED]"');
});
it("should redact secret access key in rclone command", () => {
const cmd =
'rclone rcat --s3-access-key-id="key" --s3-secret-access-key="supersecret" :s3:bucket/file.gz';
const redacted = redactRcloneCredentials(cmd);
expect(redacted).not.toContain("supersecret");
expect(redacted).toContain('--s3-secret-access-key="[REDACTED]"');
});
it("should redact both credentials simultaneously", () => {
const cmd =
'rclone lsf --s3-access-key-id="AKIA123" --s3-secret-access-key="secret456" --s3-region="us-east-1" :s3:bucket/';
const redacted = redactRcloneCredentials(cmd);
expect(redacted).not.toContain("AKIA123");
expect(redacted).not.toContain("secret456");
expect(redacted).toContain('--s3-region="us-east-1"');
});
it("should not modify non-credential flags", () => {
const cmd =
'rclone rcat --s3-region="eu-west-1" --s3-endpoint="https://s3.example.com" --s3-no-check-bucket :s3:bucket/file.gz';
const redacted = redactRcloneCredentials(cmd);
expect(redacted).toBe(cmd);
});
it("should handle commands with no credentials", () => {
const cmd = "rclone lsf :s3:bucket/";
expect(redactRcloneCredentials(cmd)).toBe(cmd);
});
it("should handle error strings containing credentials", () => {
const errorStr =
'Error: Command failed: rclone lsf --s3-access-key-id="MYKEY" --s3-secret-access-key="MYSECRET" :s3:bucket/';
const redacted = redactRcloneCredentials(errorStr);
expect(redacted).not.toContain("MYKEY");
expect(redacted).not.toContain("MYSECRET");
expect(redacted).toContain("[REDACTED]");
});
});

View File

@@ -0,0 +1,323 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
eq: vi.fn((field: string, value: unknown) => ({ field, value })),
and: vi.fn((...conditions: Array<{ field: string; value: unknown }>) => ({
conditions,
})),
githubFindFirst: vi.fn(),
applicationsFindMany: vi.fn(),
composeFindMany: vi.fn(),
queueAdd: vi.fn(),
verify: vi.fn(),
shouldDeploy: vi.fn(),
}));
vi.mock("drizzle-orm", () => ({
eq: mocks.eq,
and: mocks.and,
}));
vi.mock("@/server/db/schema", () => ({
applications: {
sourceType: "application.sourceType",
autoDeploy: "application.autoDeploy",
triggerType: "application.triggerType",
branch: "application.branch",
repository: "application.repository",
owner: "application.owner",
githubId: "application.githubId",
isPreviewDeploymentsActive: "application.isPreviewDeploymentsActive",
},
compose: {
sourceType: "compose.sourceType",
autoDeploy: "compose.autoDeploy",
triggerType: "compose.triggerType",
branch: "compose.branch",
repository: "compose.repository",
owner: "compose.owner",
githubId: "compose.githubId",
},
github: {
githubInstallationId: "github.githubInstallationId",
},
}));
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
github: {
findFirst: mocks.githubFindFirst,
},
applications: {
findMany: mocks.applicationsFindMany,
},
compose: {
findMany: mocks.composeFindMany,
},
},
},
}));
vi.mock("@dokploy/server", () => ({
IS_CLOUD: false,
shouldDeploy: mocks.shouldDeploy,
checkUserRepositoryPermissions: vi.fn(),
createPreviewDeployment: vi.fn(),
createSecurityBlockedComment: vi.fn(),
findGithubById: vi.fn(),
findPreviewDeploymentByApplicationId: vi.fn(),
findPreviewDeploymentsByPullRequestId: vi.fn(),
getBitbucketHeaders: vi.fn(() => ({})),
removePreviewDeployment: vi.fn(),
}));
vi.mock("@octokit/webhooks", () => ({
Webhooks: vi.fn().mockImplementation(function Webhooks() {
return {
verify: mocks.verify,
};
}),
}));
vi.mock("@/server/queues/queueSetup", () => ({
myQueue: {
add: mocks.queueAdd,
},
}));
vi.mock("@/server/utils/deploy", () => ({
deploy: vi.fn(),
}));
import handler from "@/pages/api/deploy/github";
const getConditionValue = (
where: { conditions?: Array<{ field: string; value: unknown }> } | undefined,
field: string,
) => where?.conditions?.find((condition) => condition.field === field)?.value;
const createResponse = () => {
const res = {
status: vi.fn(),
json: vi.fn(),
} as unknown as NextApiResponse & {
status: ReturnType<typeof vi.fn>;
json: ReturnType<typeof vi.fn>;
};
res.status.mockImplementation(() => res);
res.json.mockImplementation(() => res);
return res;
};
const createPushRequest = (
branch: string,
owner: { login?: string; name?: string } = { login: "agentHits" },
) =>
({
headers: {
"x-hub-signature-256": "sha256=test-signature",
"x-github-event": "push",
},
body: {
installation: {
id: 12345,
},
ref: `refs/heads/${branch}`,
after: "abc123",
head_commit: {
message: "fix: trigger deployment",
},
commits: [
{
modified: ["src/index.ts"],
},
],
repository: {
name: "dokploy",
full_name: "agentHits/dokploy",
clone_url: "https://github.com/agentHits/dokploy.git",
html_url: "https://github.com/agentHits/dokploy",
owner,
},
},
}) as unknown as NextApiRequest;
const createTagRequest = (tagName: string) => {
const req = createPushRequest("main") as unknown as {
body: { ref: string; head_commit: { message: string } };
};
req.body.ref = `refs/tags/${tagName}`;
req.body.head_commit.message = `release: ${tagName}`;
return req as unknown as NextApiRequest;
};
describe("GitHub app webhook auto-deploy", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.githubFindFirst.mockResolvedValue({
githubId: "github-provider-id",
githubInstallationId: 12345,
githubWebhookSecret: "webhook-secret",
});
mocks.verify.mockResolvedValue(true);
mocks.shouldDeploy.mockReturnValue(true);
mocks.composeFindMany.mockResolvedValue([]);
mocks.queueAdd.mockResolvedValue({ id: "job-id" });
mocks.applicationsFindMany.mockImplementation(({ where }) => {
const matches =
getConditionValue(where, "application.sourceType") === "github" &&
getConditionValue(where, "application.autoDeploy") === true &&
getConditionValue(where, "application.triggerType") === "push" &&
getConditionValue(where, "application.branch") === "main" &&
getConditionValue(where, "application.repository") === "dokploy" &&
getConditionValue(where, "application.owner") === "agentHits" &&
getConditionValue(where, "application.githubId") ===
"github-provider-id";
return Promise.resolve(
matches
? [
{
applicationId: "application-id",
serverId: null,
watchPaths: null,
},
]
: [],
);
});
});
it("matches push events using repository owner name when available", async () => {
const res = createResponse();
await handler(
createPushRequest("main", {
login: "agentHits-login",
name: "agentHits",
}),
res,
);
expect(mocks.queueAdd).toHaveBeenCalledWith(
"deployments",
expect.objectContaining({
applicationId: "application-id",
applicationType: "application",
type: "deploy",
}),
expect.objectContaining({
removeOnComplete: true,
removeOnFail: true,
}),
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ message: "Deployed 1 apps" });
});
it("matches compose push events using repository owner login fallback", async () => {
mocks.applicationsFindMany.mockResolvedValue([]);
mocks.composeFindMany.mockImplementation(({ where }) => {
const matches =
getConditionValue(where, "compose.sourceType") === "github" &&
getConditionValue(where, "compose.autoDeploy") === true &&
getConditionValue(where, "compose.triggerType") === "push" &&
getConditionValue(where, "compose.branch") === "main" &&
getConditionValue(where, "compose.repository") === "dokploy" &&
getConditionValue(where, "compose.owner") === "agentHits" &&
getConditionValue(where, "compose.githubId") === "github-provider-id";
return Promise.resolve(
matches
? [
{
composeId: "compose-id",
serverId: null,
watchPaths: null,
},
]
: [],
);
});
const res = createResponse();
await handler(createPushRequest("main"), res);
expect(mocks.queueAdd).toHaveBeenCalledWith(
"deployments",
expect.objectContaining({
applicationType: "compose",
composeId: "compose-id",
type: "deploy",
}),
expect.objectContaining({
removeOnComplete: true,
removeOnFail: true,
}),
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ message: "Deployed 1 apps" });
});
it("matches tag events using repository owner login fallback", async () => {
mocks.applicationsFindMany.mockImplementation(({ where }) => {
const matches =
getConditionValue(where, "application.sourceType") === "github" &&
getConditionValue(where, "application.autoDeploy") === true &&
getConditionValue(where, "application.triggerType") === "tag" &&
getConditionValue(where, "application.repository") === "dokploy" &&
getConditionValue(where, "application.owner") === "agentHits" &&
getConditionValue(where, "application.githubId") ===
"github-provider-id";
return Promise.resolve(
matches
? [
{
applicationId: "application-id",
serverId: null,
},
]
: [],
);
});
const res = createResponse();
await handler(createTagRequest("v1.0.0"), res);
expect(mocks.queueAdd).toHaveBeenCalledWith(
"deployments",
expect.objectContaining({
applicationId: "application-id",
applicationType: "application",
titleLog: "Tag created: v1.0.0",
type: "deploy",
}),
expect.objectContaining({
removeOnComplete: true,
removeOnFail: true,
}),
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: "Deployed 1 apps based on tag v1.0.0",
});
});
it("does not deploy when the pushed branch does not match", async () => {
const res = createResponse();
await handler(createPushRequest("feature"), res);
expect(mocks.queueAdd).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ message: "No apps to deploy" });
});
});

View File

@@ -183,4 +183,29 @@ describe("legacy boolean overrides for member", () => {
memberToReturn = mockMemberData("member"); memberToReturn = mockMemberData("member");
await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow(); await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow();
}); });
it("member passes gitProviders.create with canAccessToGitProviders=true", async () => {
memberToReturn = mockMemberData("member", {
canAccessToGitProviders: true,
});
await expect(
checkPermission(ctx, { gitProviders: ["create"] }),
).resolves.toBeUndefined();
});
it("member passes gitProviders.delete with canAccessToGitProviders=true", async () => {
memberToReturn = mockMemberData("member", {
canAccessToGitProviders: true,
});
await expect(
checkPermission(ctx, { gitProviders: ["delete"] }),
).resolves.toBeUndefined();
});
it("member fails gitProviders.create with canAccessToGitProviders=false", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { gitProviders: ["create"] }),
).rejects.toThrow();
});
}); });

View File

@@ -143,6 +143,24 @@ describe("free-tier resources for member", () => {
const perms = await resolvePermissions(ctx); const perms = await resolvePermissions(ctx);
expect(perms.docker.read).toBe(true); expect(perms.docker.read).toBe(true);
}); });
it("member gets gitProviders create/delete=false without legacy override", async () => {
memberToReturn = mockMemberData("member");
const perms = await resolvePermissions(ctx);
expect(perms.gitProviders.read).toBe(false);
expect(perms.gitProviders.create).toBe(false);
expect(perms.gitProviders.delete).toBe(false);
});
it("member gets gitProviders read/create/delete=true with canAccessToGitProviders", async () => {
memberToReturn = mockMemberData("member", {
canAccessToGitProviders: true,
});
const perms = await resolvePermissions(ctx);
expect(perms.gitProviders.read).toBe(true);
expect(perms.gitProviders.create).toBe(true);
expect(perms.gitProviders.delete).toBe(true);
});
}); });
describe("free-tier resources for owner", () => { describe("free-tier resources for owner", () => {

View File

@@ -0,0 +1,148 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const hasValidLicense = vi.fn();
const getWebServerSettings = vi.fn();
const findFirstOrg = vi.fn();
const findFirstServer = vi.fn();
vi.mock("@dokploy/server/db", () => ({
db: {
query: {
organization: {
findFirst: (...args: unknown[]) => findFirstOrg(...args),
},
server: {
findFirst: (...args: unknown[]) => findFirstServer(...args),
},
},
},
}));
vi.mock("@dokploy/server/db/schema", () => ({
organization: {},
server: {},
}));
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: (...args: unknown[]) => hasValidLicense(...args),
}));
vi.mock("@dokploy/server/services/web-server-settings", () => ({
getWebServerSettings: (...args: unknown[]) => getWebServerSettings(...args),
}));
vi.mock("drizzle-orm", () => ({ eq: vi.fn() }));
import {
assertBuildsConcurrencyAllowed,
resolveBuildsConcurrency,
} from "../../server/queues/concurrency";
import { LOCAL_PARTITION } from "../../server/queues/in-memory-queue";
describe("resolveBuildsConcurrency (enterprise gating)", () => {
beforeEach(() => {
vi.clearAllMocks();
findFirstOrg.mockResolvedValue({ id: "org-1" });
});
describe("local web server partition", () => {
it("returns the configured concurrency when licensed", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 5 });
hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(5);
});
it("clamps to the free max (2) when there is no valid license", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 10 });
hasValidLicense.mockResolvedValue(false);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
});
it("allows the free max (2) without a license", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 2 });
hasValidLicense.mockResolvedValue(false);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
});
it("does not cap the value when licensed (N allowed)", async () => {
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 999 });
hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(
999,
);
});
it("defaults to 1 when settings are missing", async () => {
getWebServerSettings.mockResolvedValue(undefined);
hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
});
});
describe("remote server partition", () => {
it("returns the server concurrency when its org is licensed", async () => {
findFirstServer.mockResolvedValue({
buildsConcurrency: 4,
organizationId: "org-1",
});
hasValidLicense.mockResolvedValue(true);
await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(4);
expect(hasValidLicense).toHaveBeenCalledWith("org-1");
});
it("clamps to the free max (2) when the server org is not licensed", async () => {
findFirstServer.mockResolvedValue({
buildsConcurrency: 8,
organizationId: "org-1",
});
hasValidLicense.mockResolvedValue(false);
await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(2);
});
it("defaults to 1 for an unknown server", async () => {
findFirstServer.mockResolvedValue(undefined);
await expect(resolveBuildsConcurrency("ghost")).resolves.toBe(1);
});
});
it("falls back to 1 if resolution throws", async () => {
getWebServerSettings.mockRejectedValue(new Error("db down"));
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
});
});
describe("assertBuildsConcurrencyAllowed", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("allows up to the free max (2) without checking the license", async () => {
await expect(
assertBuildsConcurrencyAllowed(2, "org-1"),
).resolves.toBeUndefined();
expect(hasValidLicense).not.toHaveBeenCalled();
});
it("allows more than 2 when licensed", async () => {
hasValidLicense.mockResolvedValue(true);
await expect(
assertBuildsConcurrencyAllowed(5, "org-1"),
).resolves.toBeUndefined();
});
it("rejects more than 2 without a license", async () => {
hasValidLicense.mockResolvedValue(false);
await expect(assertBuildsConcurrencyAllowed(3, "org-1")).rejects.toThrow(
/enterprise license/i,
);
});
});

View File

@@ -0,0 +1,337 @@
import { beforeEach, describe, expect, it } from "vitest";
import {
getGroup,
getPartition,
InMemoryQueue,
LOCAL_PARTITION,
} from "../../server/queues/in-memory-queue";
import type { DeploymentJob } from "../../server/queues/queue-types";
const appJob = (applicationId: string, serverId?: string): DeploymentJob => ({
applicationId,
titleLog: "deploy",
descriptionLog: "",
type: "deploy",
applicationType: "application",
serverId,
});
const composeJob = (composeId: string, serverId?: string): DeploymentJob => ({
composeId,
titleLog: "deploy",
descriptionLog: "",
type: "deploy",
applicationType: "compose",
serverId,
});
/** A controllable async task: resolves only when `release()` is called. */
const deferred = () => {
let resolve!: () => void;
const promise = new Promise<void>((r) => {
resolve = r;
});
return { promise, release: resolve };
};
const flush = () => new Promise((r) => setTimeout(r, 0));
describe("getPartition / getGroup", () => {
it("partitions by serverId, falling back to the local partition", () => {
expect(getPartition(appJob("a"))).toBe(LOCAL_PARTITION);
expect(getPartition(appJob("a", "server-1"))).toBe("server-1");
});
it("groups applications and compose by their id", () => {
expect(getGroup(appJob("a"))).toBe("application:a");
expect(getGroup(composeJob("c"))).toBe("compose:c");
});
});
describe("InMemoryQueue concurrency", () => {
let nowValue = 0;
const now = () => ++nowValue;
beforeEach(() => {
nowValue = 0;
});
it("runs different applications concurrently up to the limit", async () => {
const tasks = new Map<string, ReturnType<typeof deferred>>();
const started: string[] = [];
const queue = new InMemoryQueue({ resolveConcurrency: () => 2, now });
queue.process(async (job) => {
const id = (job.data as any).applicationId;
started.push(id);
const d = deferred();
tasks.set(id, d);
await d.promise;
});
await queue.run();
await queue.add(appJob("a"));
await queue.add(appJob("b"));
await queue.add(appJob("c"));
await flush();
// Concurrency 2 -> only a and b start, c waits.
expect(started).toEqual(["a", "b"]);
tasks.get("a")!.release();
await flush();
// A slot freed -> c starts.
expect(started).toEqual(["a", "b", "c"]);
});
it("serializes jobs of the same application (per-group FIFO)", async () => {
const tasks: Array<ReturnType<typeof deferred>> = [];
const started: number[] = [];
let counter = 0;
const queue = new InMemoryQueue({ resolveConcurrency: () => 5, now });
queue.process(async () => {
started.push(++counter);
const d = deferred();
tasks.push(d);
await d.promise;
});
await queue.run();
// Two deploys of the SAME app, even with concurrency 5.
await queue.add(appJob("same"));
await queue.add(appJob("same"));
await flush();
// Only the first one runs; the second waits for the group to free.
expect(started).toEqual([1]);
tasks[0]!.release();
await flush();
expect(started).toEqual([1, 2]);
});
it("isolates concurrency per server partition", async () => {
const started: string[] = [];
const tasks = new Map<string, ReturnType<typeof deferred>>();
// server-1 allows 1, server-2 allows 1, but they are independent.
const queue = new InMemoryQueue({
resolveConcurrency: () => 1,
now,
});
queue.process(async (job) => {
const id = `${job.data.serverId}:${(job.data as any).applicationId}`;
started.push(id);
const d = deferred();
tasks.set(id, d);
await d.promise;
});
await queue.run();
await queue.add(appJob("a", "server-1"));
await queue.add(appJob("b", "server-2"));
await flush();
// One per partition runs in parallel despite concurrency 1 each.
expect(started.sort()).toEqual(["server-1:a", "server-2:b"]);
});
it("honors a different concurrency per server", async () => {
const started: string[] = [];
const tasks = new Map<string, ReturnType<typeof deferred>>();
// server-fast allows 2, server-slow allows 1.
const queue = new InMemoryQueue({
resolveConcurrency: (partition) => (partition === "server-fast" ? 2 : 1),
now,
});
queue.process(async (job) => {
const id = `${job.data.serverId}:${(job.data as any).applicationId}`;
started.push(id);
const d = deferred();
tasks.set(id, d);
await d.promise;
});
await queue.run();
await queue.add(appJob("a", "server-fast"));
await queue.add(appJob("b", "server-fast"));
await queue.add(appJob("c", "server-slow"));
await queue.add(appJob("d", "server-slow"));
await flush();
// server-fast runs 2 in parallel; server-slow only 1.
expect(started.sort()).toEqual([
"server-fast:a",
"server-fast:b",
"server-slow:c",
]);
// Free a server-slow slot -> its queued app starts.
tasks.get("server-slow:c")!.release();
await flush();
expect(started).toContain("server-slow:d");
});
it("serializes the same app on a server even with spare concurrency", async () => {
const started: number[] = [];
const tasks: Array<ReturnType<typeof deferred>> = [];
let counter = 0;
// Plenty of room (concurrency 2) but two deploys of the SAME app.
const queue = new InMemoryQueue({ resolveConcurrency: () => 2, now });
queue.process(async () => {
started.push(++counter);
const d = deferred();
tasks.push(d);
await d.promise;
});
await queue.run();
await queue.add(appJob("app-x", "server-1"));
await queue.add(appJob("app-x", "server-1"));
await flush();
// Only one build of app-x runs despite 2 free slots.
expect(started).toEqual([1]);
tasks[0]!.release();
await flush();
expect(started).toEqual([1, 2]);
});
it("clamps concurrency below 1 up to 1 (license-disabled behaviour)", async () => {
const started: string[] = [];
const tasks = new Map<string, ReturnType<typeof deferred>>();
// Simulate a non-licensed resolver returning 0 — must still run 1.
const queue = new InMemoryQueue({ resolveConcurrency: () => 0, now });
queue.process(async (job) => {
const id = (job.data as any).applicationId;
started.push(id);
const d = deferred();
tasks.set(id, d);
await d.promise;
});
await queue.run();
await queue.add(appJob("a"));
await queue.add(appJob("b"));
await flush();
expect(started).toEqual(["a"]);
});
it("picks up concurrency changes between scheduling ticks", async () => {
const started: string[] = [];
const tasks = new Map<string, ReturnType<typeof deferred>>();
let limit = 1;
const queue = new InMemoryQueue({
resolveConcurrency: () => limit,
now,
});
queue.process(async (job) => {
const id = (job.data as any).applicationId;
started.push(id);
const d = deferred();
tasks.set(id, d);
await d.promise;
});
await queue.run();
await queue.add(appJob("a"));
await queue.add(appJob("b"));
await flush();
expect(started).toEqual(["a"]);
// Raise the limit (e.g. license activated) and release the running job
// so a new tick observes the new concurrency.
limit = 2;
tasks.get("a")!.release();
await flush();
expect(started).toContain("b");
});
});
describe("InMemoryQueue job management", () => {
it("lists waiting jobs and removes them by predicate", async () => {
const block = deferred();
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
queue.process(async () => {
await block.promise;
});
await queue.run();
await queue.add(appJob("running"));
await queue.add(appJob("waiting-1"));
await queue.add(composeJob("waiting-2"));
await flush();
const waiting = await queue.getJobs(["waiting"]);
expect(waiting.map((j) => j.data)).toHaveLength(2);
const removed = queue.removeWaiting(
(data) => (data as any).applicationId === "waiting-1",
);
expect(removed).toBe(1);
const after = await queue.getJobs(["waiting"]);
expect(after).toHaveLength(1);
});
it("clears all waiting jobs", async () => {
const block = deferred();
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
queue.process(async () => {
await block.promise;
});
await queue.run();
await queue.add(appJob("running"));
await queue.add(appJob("waiting-1"));
await queue.add(appJob("waiting-2"));
await flush();
expect(queue.clearWaiting()).toBe(2);
expect(await queue.getJobs(["waiting"])).toHaveLength(0);
});
it("starts processing as soon as a processor is registered", async () => {
const started: string[] = [];
const queue = new InMemoryQueue({ resolveConcurrency: () => 5 });
// No processor yet -> jobs queue but do not run.
await queue.add(appJob("a"));
await flush();
expect(started).toEqual([]);
// Registering the processor auto-starts the queue (no separate run()).
queue.process(async (job) => {
started.push((job.data as any).applicationId);
});
await flush();
expect(started).toEqual(["a"]);
});
it("continues scheduling after a job throws", async () => {
const started: string[] = [];
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
queue.process(async (job) => {
const id = (job.data as any).applicationId;
started.push(id);
if (id === "a") throw new Error("boom");
});
await queue.run();
await queue.add(appJob("a"));
await queue.add(appJob("b"));
await flush();
expect(started).toEqual(["a", "b"]);
});
});

View File

@@ -0,0 +1,75 @@
import { apiCreateRegistry, apiTestRegistry } from "@dokploy/server/db/schema";
import { describe, expect, it } from "vitest";
describe("Registry Schema - Username case preservation (#4632)", () => {
const validBase = {
registryName: "AWS ECR",
password: "dXNlcm5hbWU6cGFzc3dvcmQ=", // dummy base64 token
registryUrl: "123456789.dkr.ecr.us-east-1.amazonaws.com",
registryType: "cloud" as const,
imagePrefix: null,
};
it("should preserve uppercase username (AWS ECR requires 'AWS')", () => {
const result = apiCreateRegistry.safeParse({
...validBase,
username: "AWS",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.username).toBe("AWS");
}
});
it("should not lowercase mixed-case usernames", () => {
const result = apiCreateRegistry.safeParse({
...validBase,
username: "MyUser",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.username).toBe("MyUser");
}
});
it("should still trim whitespace from username", () => {
const result = apiCreateRegistry.safeParse({
...validBase,
username: " AWS ",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.username).toBe("AWS");
}
});
it("should reject empty username", () => {
const result = apiCreateRegistry.safeParse({
...validBase,
username: "",
});
expect(result.success).toBe(false);
});
it("should also preserve case in apiTestRegistry", () => {
const result = apiTestRegistry.safeParse({
...validBase,
username: "AWS",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.username).toBe("AWS");
}
});
it("should accept lowercase usernames too (backward compat)", () => {
const result = apiCreateRegistry.safeParse({
...validBase,
username: "myuser",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.username).toBe("myuser");
}
});
});

View File

@@ -0,0 +1,98 @@
import { execFileSync, execSync } from "node:child_process";
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { defaultCommand, reportDockerVersion } from "@dokploy/server";
import { describe, expect, it } from "vitest";
const resolveBin = (name: string) =>
execSync(`command -v ${name}`, { encoding: "utf8" }).trim();
/**
* Build a sandbox PATH so `command -v docker` only sees our fake docker
* binary (or nothing), regardless of what the host has installed.
*/
const makeSandbox = (dockerShim?: string) => {
const dir = mkdtempSync(path.join(tmpdir(), "dokploy-server-setup-"));
for (const tool of ["awk", "tr"]) {
const shim = path.join(dir, tool);
writeFileSync(shim, `#!/bin/sh\nexec ${resolveBin(tool)} "$@"\n`);
chmodSync(shim, 0o755);
}
if (dockerShim) {
const shim = path.join(dir, "docker");
writeFileSync(shim, dockerShim);
chmodSync(shim, 0o755);
}
return dir;
};
const runReport = (sandboxPath: string) => {
const script = [
"DOCKER_VERSION=28.5.0",
reportDockerVersion(),
'echo "$DOCKER_VERSION_REPORT"',
].join("\n");
return execFileSync(resolveBin("bash"), ["-c", script], {
encoding: "utf8",
env: { ...process.env, PATH: sandboxPath },
})
.trim()
.split("\n")
.pop();
};
describe("reportDockerVersion", () => {
it("reports the engine version when docker and its daemon are available", () => {
const sandbox = makeSandbox(
[
"#!/bin/sh",
'if [ "$1" = "--version" ]; then',
' echo "Docker version 25.0.0, build aaaaaaa"',
" exit 0",
"fi",
'if [ "$1" = "version" ]; then',
' echo "29.4.3"',
" exit 0",
"fi",
"exit 1",
].join("\n"),
);
expect(runReport(sandbox)).toBe("29.4.3 (already installed)");
});
it("falls back to the client version when the daemon is unreachable", () => {
const sandbox = makeSandbox(
[
"#!/bin/sh",
'if [ "$1" = "--version" ]; then',
' echo "Docker version 29.4.3, build 055a478"',
" exit 0",
"fi",
'echo "Cannot connect to the Docker daemon" >&2',
"exit 1",
].join("\n"),
);
expect(runReport(sandbox)).toBe("29.4.3 (already installed)");
});
it("reports the pinned version to be installed when docker is missing", () => {
expect(runReport(makeSandbox())).toBe("28.5.0 (will be installed)");
});
});
describe("defaultCommand", () => {
it.each([false, true])(
"prints the detected Docker version in the setup banner (isBuildServer=%s)",
(isBuildServer) => {
const script = defaultCommand(isBuildServer);
expect(script).toContain(reportDockerVersion());
expect(script).toContain(
'echo "| Docker | $DOCKER_VERSION_REPORT"',
);
expect(script).not.toContain(
'echo "| Docker | $DOCKER_VERSION"',
);
},
);
});

View File

@@ -25,6 +25,7 @@ const baseSettings: WebServerSettings = {
letsEncryptEmail: null, letsEncryptEmail: null,
sshPrivateKey: null, sshPrivateKey: null,
enableDockerCleanup: false, enableDockerCleanup: false,
buildsConcurrency: 1,
logCleanupCron: null, logCleanupCron: null,
metricsConfig: { metricsConfig: {
containers: { containers: {

View File

@@ -0,0 +1,46 @@
import { VALID_HOSTNAME_REGEX } from "@dokploy/server";
import { describe, expect, it } from "vitest";
describe("VALID_HOSTNAME_REGEX", () => {
it.each([
"example.com",
"sub.example.com",
"bbn-client.example.com",
"a.b.c.example.co",
"xn--80ak6aa92e.com",
"123.example.com",
])("accepts valid hostname %s", (host) => {
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(true);
});
it.each([
"bbn_client.example.com",
"-example.com",
"example-.com",
"example",
"exa mple.com",
"example..com",
"",
`a${"a".repeat(63)}.com`,
])("rejects invalid hostname %s", (host) => {
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(false);
});
// IDNs (Cyrillic, German umlauts, etc.) must be submitted in their
// ACME/punycode form ("xn--...") — that's what Let's Encrypt issues
// certificates for, so raw Unicode labels are rejected here.
it.each(["пример.рф", "bücher.de", "日本語.jp"])(
"rejects raw unicode IDN %s",
(host) => {
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(false);
},
);
it.each([
"xn--e1afmkfd.xn--p1ai", // punycode for пример.рф
"xn--bcher-kva.de", // punycode for bücher.de
"xn--wgv71a119e.jp", // punycode for 日本語.jp
])("accepts punycode-encoded IDN %s", (host) => {
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(true);
});
});

View File

@@ -1,17 +1,22 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "default", "style": "radix-nova",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.ts", "config": "",
"css": "styles/globals.css", "css": "styles/globals.css",
"baseColor": "zinc", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils" "utils": "@/lib/utils"
} },
"iconLibrary": "lucide",
"rtl": false,
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
} }

View File

@@ -156,7 +156,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
<div className="flex gap-4 h-[60vh] py-4"> <div className="flex gap-4 h-[60vh] py-4">
{/* Left Column - Menu */} {/* Left Column - Menu */}
<div className="w-64 flex-shrink-0 border-r pr-4 overflow-y-auto"> <div className="w-64 shrink-0 border-r pr-4 overflow-y-auto">
<nav className="space-y-1"> <nav className="space-y-1">
<TooltipProvider> <TooltipProvider>
{menuItems.map((item) => ( {menuItems.map((item) => (

View File

@@ -229,7 +229,7 @@ export const ShowImport = ({ composeId }: Props) => {
(domain, index) => ( (domain, index) => (
<div <div
key={index} key={index}
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm" className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
> >
<div className="font-medium"> <div className="font-medium">
{domain.serviceName} {domain.serviceName}

View File

@@ -246,7 +246,7 @@ export const HandleRedirect = ({
control={form.control} control={form.control}
name="permanent" name="permanent"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Permanent</FormLabel> <FormLabel>Permanent</FormLabel>
<FormDescription> <FormDescription>

View File

@@ -53,7 +53,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
</div> </div>
) : ( ) : (
<div className="flex flex-col pt-2 relative"> <div className="flex flex-col pt-2 relative">
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto"> <div className="flex flex-col gap-6 max-h-140 min-h-40 overflow-y-auto">
<CodeEditor <CodeEditor
lineWrapping lineWrapping
value={data || "Empty"} value={data || "Empty"}

View File

@@ -155,7 +155,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<CodeEditor <CodeEditor
lineWrapping lineWrapping
wrapperClassName="h-[35rem] font-mono" wrapperClassName="h-140 font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:

View File

@@ -220,7 +220,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="bind" htmlFor="bind"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer" className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
> >
Bind Mount Bind Mount
</Label> </Label>
@@ -240,7 +240,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="volume" htmlFor="volume"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer" className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
> >
Volume Mount Volume Mount
</Label> </Label>
@@ -264,7 +264,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="file" htmlFor="file"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer" className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
> >
File Mount File Mount
</Label> </Label>
@@ -324,7 +324,7 @@ export const AddVolumes = ({
control={form.control} control={form.control}
name="content" name="content"
render={({ field }) => ( render={({ field }) => (
<FormItem className="max-w-full max-w-[45rem]"> <FormItem className="max-w-full max-w-180">
<FormLabel>Content</FormLabel> <FormLabel>Content</FormLabel>
<FormControl> <FormControl>
<FormControl> <FormControl>

View File

@@ -111,7 +111,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && ( {mount.type === "file" && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">Content</span> <span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces"> <span className="text-sm text-muted-foreground line-clamp-10 whitespace-break-spaces">
{mount.content} {mount.content}
</span> </span>
</div> </div>

View File

@@ -253,7 +253,7 @@ export const UpdateVolume = ({
control={form.control} control={form.control}
name="content" name="content"
render={({ field }) => ( render={({ field }) => (
<FormItem className="w-full max-w-[45rem]"> <FormItem className="w-full max-w-180">
<FormLabel>Content</FormLabel> <FormLabel>Content</FormLabel>
<FormControl> <FormControl>
<FormControl> <FormControl>

View File

@@ -191,7 +191,7 @@ export const ShowDeployment = ({
<div <div
ref={scrollRef} ref={scrollRef}
onScroll={handleScroll} onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar" className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-background rounded custom-logs-scrollbar"
> >
{" "} {" "}
{filteredLogs.length > 0 ? ( {filteredLogs.length > 0 ? (

View File

@@ -147,7 +147,7 @@ export const ShowDeployments = ({
}, []); }, []);
return ( return (
<Card className="bg-background border-none"> <Card className="bg-background border-0">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2"> <CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<CardTitle className="text-xl">Deployments</CardTitle> <CardTitle className="text-xl">Deployments</CardTitle>
@@ -233,7 +233,6 @@ export const ShowDeployments = ({
<span>Webhook URL: </span> <span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<Badge <Badge
role="button"
tabIndex={0} tabIndex={0}
aria-label="Copy webhook URL to clipboard" aria-label="Copy webhook URL to clipboard"
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all" className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all"
@@ -301,7 +300,7 @@ export const ShowDeployments = ({
</span> </span>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="break-words text-sm text-muted-foreground whitespace-pre-wrap"> <span className="wrap-break-word text-sm text-muted-foreground whitespace-pre-wrap">
{isExpanded || !needsTruncation {isExpanded || !needsTruncation
? titleText ? titleText
: truncateDescription(titleText)} : truncateDescription(titleText)}

View File

@@ -1,3 +1,7 @@
import {
INVALID_HOSTNAME_MESSAGE,
VALID_HOSTNAME_REGEX,
} from "@dokploy/server/utils/hostname-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, Dices, RefreshCw, X } from "lucide-react"; import { DatabaseZap, Dices, RefreshCw, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -53,7 +57,10 @@ export const domain = z
.refine((val) => val === val.trim(), { .refine((val) => val === val.trim(), {
message: "Domain name cannot have leading or trailing spaces", message: "Domain name cannot have leading or trailing spaces",
}) })
.transform((val) => val.trim()), .transform((val) => val.trim())
.refine((val) => VALID_HOSTNAME_REGEX.test(val), {
message: INVALID_HOSTNAME_MESSAGE,
}),
path: z.string().min(1).optional(), path: z.string().min(1).optional(),
internalPath: z.string().optional(), internalPath: z.string().optional(),
stripPath: z.boolean().optional(), stripPath: z.boolean().optional(),
@@ -349,10 +356,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
{domainType === "compose" && ( {domainType === "compose" && (
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
{errorServices && ( {errorServices && (
<AlertBlock <AlertBlock type="warning" className="wrap-anywhere">
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message} {errorServices?.message}
</AlertBlock> </AlertBlock>
)} )}
@@ -420,7 +424,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Fetch: Will clone the repository and Fetch: Will clone the repository and
@@ -450,7 +454,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Cache: If you previously deployed this Cache: If you previously deployed this
@@ -488,7 +492,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
{isManualInput {isManualInput
@@ -565,7 +569,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p>Generate sslip.io domain</p> <p>Generate sslip.io domain</p>
</TooltipContent> </TooltipContent>
@@ -618,7 +622,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
control={form.control} control={form.control}
name="stripPath" name="stripPath"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Strip Path</FormLabel> <FormLabel>Strip Path</FormLabel>
<FormDescription> <FormDescription>
@@ -662,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
control={form.control} control={form.control}
name="useCustomEntrypoint" name="useCustomEntrypoint"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel> <FormLabel>Custom Entrypoint</FormLabel>
<FormDescription> <FormDescription>
@@ -711,7 +715,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
control={form.control} control={form.control}
name="https" name="https"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel> <FormLabel>HTTPS</FormLabel>
<FormDescription> <FormDescription>
@@ -763,6 +767,37 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<SelectItem value={"custom"}>Custom</SelectItem> <SelectItem value={"custom"}>Custom</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription>
{field.value === "none" && (
<>
<strong>None</strong> serves TLS using any
certificate you created in the{" "}
<Link
href="/dashboard/settings/certificates"
className="text-primary"
>
Certificates
</Link>{" "}
section whose CN/SAN matches this host —
Traefik selects it automatically via SNI.
</>
)}
{field.value === "letsencrypt" && (
<>
<strong>Let's Encrypt</strong> auto-provisions
a certificate automatically for this host.
</>
)}
{field.value === "custom" && (
<>
<strong>Custom</strong> uses a Traefik cert
resolver by name (defined in your static
configuration).
</>
)}
{!field.value &&
"Select a certificate provider to see how TLS will be served for this host."}
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
); );
@@ -777,10 +812,19 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>Custom Certificate Resolver</FormLabel> <FormLabel>Custom Certificate Resolver</FormLabel>
<FormDescription>
Enter the <strong>name</strong> of a Traefik
cert resolver defined in your static
configuration (e.g. <code>letsencrypt</code>)
not certificate or private key content. To use a
certificate you pasted in the Certificates
section, choose <strong>None</strong> instead
and Traefik will match it by SNI.
</FormDescription>
<FormControl> <FormControl>
<Input <Input
className="w-full" className="w-full"
placeholder="Enter your custom certificate resolver" placeholder="e.g. letsencrypt"
{...field} {...field}
value={field.value || ""} value={field.value || ""}
onChange={(e) => { onChange={(e) => {

View File

@@ -189,7 +189,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="createEnvFile" name="createEnvFile"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Create Environment File</FormLabel> <FormLabel>Create Environment File</FormLabel>
<FormDescription> <FormDescription>

View File

@@ -188,6 +188,9 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormLabel>Bitbucket Account</FormLabel> <FormLabel>Bitbucket Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -196,7 +199,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -245,7 +247,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -333,7 +335,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -498,7 +500,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -305,7 +305,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -201,6 +201,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<FormLabel>Gitea Account</FormLabel> <FormLabel>Gitea Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -208,7 +211,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -258,7 +260,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -353,7 +355,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -525,7 +527,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -177,6 +177,9 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<FormLabel>Github Account</FormLabel> <FormLabel>Github Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -189,7 +192,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Github Account" /> <SelectValue placeholder="Select a Github Account">
{
githubProviders?.find(
(githubProvider) =>
githubProvider.githubId === field.value,
)?.gitProvider.name
}
</SelectValue>
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
@@ -233,7 +243,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -243,7 +253,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) => repo.name === field.value.repo,
)?.name ?? "Select repository")} )?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@@ -320,16 +330,16 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
{status === "pending" && fetchStatus === "fetching" {status === "pending" && fetchStatus === "fetching"
? "Loading...." ? "Loading...."
: field.value : field.value
? branches?.find( ? (branches?.find(
(branch) => branch.name === field.value, (branch) => branch.name === field.value,
)?.name )?.name ?? field.value)
: "Select branch"} : "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@@ -531,7 +541,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -196,6 +196,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormLabel>Gitlab Account</FormLabel> <FormLabel>Gitlab Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -205,7 +208,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -254,7 +256,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -351,7 +353,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -518,7 +520,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -154,7 +154,10 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
}} }}
> >
<div className="flex flex-row items-center justify-between w-full overflow-auto"> <div className="flex flex-row items-center justify-between w-full overflow-auto">
<TabsList className="flex gap-4 justify-start bg-transparent"> <TabsList
variant="line"
className="flex gap-4 justify-start bg-transparent"
>
<TabsTrigger <TabsTrigger
value="github" value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
@@ -94,7 +94,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Downloads the source code and performs a complete Downloads the source code and performs a complete
build build
@@ -137,7 +137,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Reload the application without rebuilding it</p> <p>Reload the application without rebuilding it</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -176,7 +176,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Only rebuilds the application without downloading new Only rebuilds the application without downloading new
code code
@@ -219,7 +219,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the application (requires a previous successful Start the application (requires a previous successful
build) build)
@@ -259,7 +259,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running application</p> <p>Stop the currently running application</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -200,7 +200,7 @@ export const AddPreviewDomain = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p>Generate sslip.io domain</p> <p>Generate sslip.io domain</p>
</TooltipContent> </TooltipContent>
@@ -249,7 +249,7 @@ export const AddPreviewDomain = ({
control={form.control} control={form.control}
name="https" name="https"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel> <FormLabel>HTTPS</FormLabel>
<FormDescription> <FormDescription>

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { import {
ExternalLink, ExternalLink,
FileText, FileText,
@@ -132,7 +132,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
<div className="p-4"> <div className="p-4">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" /> <GitPullRequest className="size-5 text-muted-foreground mt-1 shrink-0" />
<div> <div>
<div className="font-medium text-sm"> <div className="font-medium text-sm">
{deployment.pullRequestTitle} {deployment.pullRequestTitle}
@@ -152,7 +152,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
</div> </div>
<div className="pl-8 space-y-3"> <div className="pl-8 space-y-3">
<div className="relative flex-grow"> <div className="relative grow">
<Input <Input
value={deploymentUrl} value={deploymentUrl}
readOnly readOnly
@@ -244,7 +244,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent <TooltipContent
sideOffset={5} sideOffset={5}
className="z-[60]" className="z-60"
> >
<p> <p>
Rebuild the preview deployment without Rebuild the preview deployment without

View File

@@ -325,7 +325,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="previewHttps" name="previewHttps"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel> <FormLabel>HTTPS</FormLabel>
<FormDescription> <FormDescription>
@@ -431,7 +431,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="previewRequireCollaboratorPermissions" name="previewRequireCollaboratorPermissions"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs col-span-2">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel> <FormLabel>
Require Collaborator Permissions Require Collaborator Permissions

View File

@@ -355,10 +355,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
{scheduleTypeForm === "compose" && ( {scheduleTypeForm === "compose" && (
<div className="flex flex-col w-full gap-4"> <div className="flex flex-col w-full gap-4">
{errorServices && ( {errorServices && (
<AlertBlock <AlertBlock type="warning" className="wrap-anywhere">
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message} {errorServices?.message}
</AlertBlock> </AlertBlock>
)} )}
@@ -414,7 +411,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Fetch: Will clone the repository and load the Fetch: Will clone the repository and load the
@@ -444,7 +441,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Cache: If you previously deployed this compose, Cache: If you previously deployed this compose,
@@ -534,7 +531,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >

View File

@@ -73,7 +73,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
}; };
return ( return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]"> <Card className=" px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0"> <CardHeader className="px-0">
<div className="flex justify-between items-center gap-y-2 flex-wrap"> <div className="flex justify-between items-center gap-y-2 flex-wrap">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -110,12 +110,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full" className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
> >
<div className="flex items-start gap-3 w-full sm:w-auto"> <div className="flex items-start gap-3 w-full sm:w-auto">
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5"> <div className="flex shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<Clock className="size-4 text-primary/70" /> <Clock className="size-4 text-primary/70" />
</div> </div>
<div className="space-y-1.5 w-full sm:w-auto"> <div className="space-y-1.5 w-full sm:w-auto">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3"> <h3 className="text-sm font-medium leading-none wrap-anywhere line-clamp-3">
{schedule.name} {schedule.name}
</h3> </h3>
<Badge <Badge
@@ -126,7 +126,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</Badge> </Badge>
</div> </div>
{schedule.description && ( {schedule.description && (
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2"> <p className="text-xs text-muted-foreground/70 wrap-anywhere line-clamp-2">
{schedule.description} {schedule.description}
</p> </p>
)} )}
@@ -154,7 +154,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</div> </div>
{schedule.command && ( {schedule.command && (
<div className="flex items-start gap-2 max-w-full"> <div className="flex items-start gap-2 max-w-full">
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" /> <Terminal className="size-3.5 text-muted-foreground/70 shrink-0 mt-0.5" />
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]"> <code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
{schedule.command} {schedule.command}
</code> </code>

View File

@@ -349,10 +349,7 @@ export const HandleVolumeBackups = ({
<> <>
<div className="flex flex-col w-full gap-4"> <div className="flex flex-col w-full gap-4">
{errorServices && ( {errorServices && (
<AlertBlock <AlertBlock type="warning" className="wrap-anywhere">
type="warning"
className="[overflow-wrap:anywhere]"
>
{errorServices?.message} {errorServices?.message}
</AlertBlock> </AlertBlock>
)} )}
@@ -408,7 +405,7 @@ export const HandleVolumeBackups = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Fetch: Will clone the repository and load the Fetch: Will clone the repository and load the
@@ -438,7 +435,7 @@ export const HandleVolumeBackups = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Cache: If you previously deployed this Cache: If you previously deployed this
@@ -510,11 +507,20 @@ export const HandleVolumeBackups = ({
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{mounts?.map((mount) => ( {mounts && mounts.length > 0 ? (
<SelectItem key={mount.Name} value={mount.Name || ""}> mounts.map((mount) => (
{mount.Name} <SelectItem
key={mount.Name}
value={mount.Name || ""}
>
{mount.Name}
</SelectItem>
))
) : (
<SelectItem value="none" disabled>
No volumes found
</SelectItem> </SelectItem>
))} )}
</SelectContent> </SelectContent>
</Select> </Select>
<FormDescription> <FormDescription>

View File

@@ -181,7 +181,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -263,7 +263,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >

View File

@@ -77,7 +77,7 @@ export const ShowVolumeBackups = ({
}; };
return ( return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]"> <Card className=" px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0"> <CardHeader className="px-0">
<div className="flex justify-between items-center flex-wrap gap-2"> <div className="flex justify-between items-center flex-wrap gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@@ -160,7 +160,7 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="isolatedDeployment" name="isolatedDeployment"
render={({ field }) => ( render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm"> <FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel> <FormLabel>
Enable Isolated Deployment ({data?.appName}) Enable Isolated Deployment ({data?.appName})

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -72,7 +72,7 @@ export const ComposeActions = ({ composeId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Downloads the source code and performs a complete build Downloads the source code and performs a complete build
</p> </p>
@@ -113,7 +113,7 @@ export const ComposeActions = ({ composeId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Reload the compose without rebuilding it</p> <p>Reload the compose without rebuilding it</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -154,7 +154,7 @@ export const ComposeActions = ({ composeId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the compose (requires a previous successful build) Start the compose (requires a previous successful build)
</p> </p>
@@ -193,7 +193,7 @@ export const ComposeActions = ({ composeId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running compose</p> <p>Stop the currently running compose</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -135,7 +135,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem className="overflow-auto"> <FormItem className="overflow-auto">
<FormControl className=""> <FormControl className="">
<div className="flex flex-col gap-4 w-full outline-none focus:outline-none overflow-auto"> <div className="flex flex-col gap-4 w-full outline-hidden focus:outline-hidden overflow-auto">
<CodeEditor <CodeEditor
// disabled // disabled
language="yaml" language="yaml"

View File

@@ -190,6 +190,9 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormLabel>Bitbucket Account</FormLabel> <FormLabel>Bitbucket Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -198,7 +201,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -247,7 +249,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -335,7 +337,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -502,7 +504,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -313,7 +313,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -188,6 +188,9 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<FormLabel>Gitea Account</FormLabel> <FormLabel>Gitea Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -195,7 +198,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -244,7 +246,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -331,7 +333,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -491,7 +493,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -138,7 +138,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.composeId, form]); }, [form.reset, data]);
const onSubmit = async (data: GithubProvider) => { const onSubmit = async (data: GithubProvider) => {
await mutateAsync({ await mutateAsync({
@@ -179,6 +179,9 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormLabel>Github Account</FormLabel> <FormLabel>Github Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -186,7 +189,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -234,7 +236,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -244,7 +246,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
? "Loading...." ? "Loading...."
: (repositories?.find( : (repositories?.find(
(repo) => repo.name === field.value.repo, (repo) => repo.name === field.value.repo,
)?.name ?? "Select repository")} )?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@@ -321,16 +323,16 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
{status === "pending" && fetchStatus === "fetching" {status === "pending" && fetchStatus === "fetching"
? "Loading...." ? "Loading...."
: field.value : field.value
? branches?.find( ? (branches?.find(
(branch) => branch.name === field.value, (branch) => branch.name === field.value,
)?.name )?.name ?? field.value)
: "Select branch"} : "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@@ -534,7 +536,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -199,6 +199,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormLabel>Gitlab Account</FormLabel> <FormLabel>Gitlab Account</FormLabel>
<Select <Select
onValueChange={(value) => { onValueChange={(value) => {
if (!value) {
return;
}
field.onChange(value); field.onChange(value);
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
@@ -208,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
defaultValue={field.value}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -256,7 +258,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -353,7 +355,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
" w-full justify-between !bg-input", " w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -520,7 +522,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel> <FormLabel className="mt-0!">Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -143,7 +143,10 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
}} }}
> >
<div className="flex flex-row items-center justify-between w-full overflow-auto"> <div className="flex flex-row items-center justify-between w-full overflow-auto">
<TabsList className="flex gap-4 justify-start bg-transparent"> <TabsList
variant="line"
className="flex gap-4 justify-start bg-transparent"
>
<TabsTrigger <TabsTrigger
value="github" value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"

View File

@@ -160,7 +160,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="randomize" name="randomize"
render={({ field }) => ( render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm"> <FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Apply Randomize</FormLabel> <FormLabel>Apply Randomize</FormLabel>
<FormDescription> <FormDescription>

View File

@@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview Compose Preview Compose
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-6xl max-h-[50rem]"> <DialogContent className="sm:max-w-6xl max-h-200">
<DialogHeader> <DialogHeader>
<DialogTitle>Converted Compose</DialogTitle> <DialogTitle>Converted Compose</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -67,11 +67,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
one domain must be specified for this conversion to take effect. one domain must be specified for this conversion to take effect.
</AlertBlock> </AlertBlock>
{isPending ? ( {isPending ? (
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md"> <div className="flex flex-row items-center justify-center min-h-100 border p-4 rounded-md">
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" /> <Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
</div> </div>
) : compose?.length === 5 ? ( ) : compose?.length === 5 ? (
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]"> <div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-100">
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" /> <Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-muted-foreground"> <span className="text-muted-foreground">
No converted compose data available. No converted compose data available.

View File

@@ -364,7 +364,7 @@ export const HandleBackup = ({
> >
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{errorServices && ( {errorServices && (
<AlertBlock type="warning" className="[overflow-wrap:anywhere]"> <AlertBlock type="warning" className="wrap-anywhere">
{errorServices?.message} {errorServices?.message}
</AlertBlock> </AlertBlock>
)} )}
@@ -409,7 +409,7 @@ export const HandleBackup = ({
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -528,7 +528,7 @@ export const HandleBackup = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Fetch: Will clone the repository and load the Fetch: Will clone the repository and load the
@@ -558,7 +558,7 @@ export const HandleBackup = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Cache: If you previously deployed this Cache: If you previously deployed this

View File

@@ -345,7 +345,7 @@ export const RestoreBackup = ({
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -427,7 +427,7 @@ export const RestoreBackup = ({
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full justify-between !bg-input", "w-full justify-between",
!field.value && "text-muted-foreground", !field.value && "text-muted-foreground",
)} )}
> >
@@ -622,7 +622,7 @@ export const RestoreBackup = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Fetch: Will clone the repository and load the Fetch: Will clone the repository and load the
@@ -652,7 +652,7 @@ export const RestoreBackup = ({
<TooltipContent <TooltipContent
side="left" side="left"
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-40"
> >
<p> <p>
Cache: If you previously deployed this compose, Cache: If you previously deployed this compose,

View File

@@ -44,7 +44,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
</DialogHeader> </DialogHeader>
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]"> <div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
<code> <code>
<pre className="whitespace-pre-wrap break-words"> <pre className="whitespace-pre-wrap wrap-break-word">
<CodeEditor <CodeEditor
language="json" language="json"
lineWrapping lineWrapping

View File

@@ -165,7 +165,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
) : ( ) : (
<> <>
<div className="max-h-[400px] overflow-y-auto"> <div className="max-h-[400px] overflow-y-auto">
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words"> <div className="prose prose-sm dark:prose-invert max-w-none text-sm wrap-break-word">
<ReactMarkdown>{data.analysis}</ReactMarkdown> <ReactMarkdown>{data.analysis}</ReactMarkdown>
</div> </div>
</div> </div>

View File

@@ -119,7 +119,7 @@ export function LineCountFilter({
placeholder="Number of lines" placeholder="Number of lines"
value={inputValue} value={inputValue}
onValueChange={handleInputChange} onValueChange={handleInputChange}
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50" className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
@@ -146,7 +146,7 @@ export function LineCountFilter({
<CommandPrimitive.Item <CommandPrimitive.Item
key={option.value} key={option.value}
onSelect={() => handleSelect(option.label)} onSelect={() => handleSelect(option.label)}
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground" className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
> >
<div <div
className={cn( className={cn(

View File

@@ -4,7 +4,6 @@ import { Badge } from "@/components/ui/badge";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipPortal,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
@@ -65,22 +64,20 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const tooltip = (color: string, timestamp: string | null) => { const tooltip = (color: string, timestamp: string | null) => {
const square = ( const square = (
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} /> <div className={cn("w-2 h-full shrink-0 rounded-[3px]", color)} />
); );
return timestamp ? ( return timestamp ? (
<TooltipProvider delayDuration={0} disableHoverableContent> <TooltipProvider delayDuration={0} disableHoverableContent>
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{square}</TooltipTrigger> <TooltipTrigger asChild>{square}</TooltipTrigger>
<TooltipPortal> <TooltipContent
<TooltipContent sideOffset={5}
sideOffset={5} className="bg-popover border-border z-99999"
className="bg-popover border-border z-[99999]" >
> <p className="text text-xs text-muted-foreground break-all max-w-md">
<p className="text text-xs text-muted-foreground break-all max-w-md"> <pre>{timestamp}</pre>
<pre>{timestamp}</pre> </p>
</p> </TooltipContent>
</TooltipContent>
</TooltipPortal>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
) : ( ) : (
@@ -107,7 +104,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */} {/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
{tooltip(color, rawTimestamp)} {tooltip(color, rawTimestamp)}
{!noTimestamp && ( {!noTimestamp && (
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0"> <span className="select-none pl-2 text-muted-foreground w-full sm:w-40 shrink-0">
{formattedTime} {formattedTime}
</span> </span>
)} )}

View File

@@ -26,7 +26,7 @@ export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600" className="w-full cursor-pointer text-red-500 hover:text-red-600!"
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}
> >
Remove Container Remove Container

View File

@@ -100,7 +100,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="w-full relative z-[5]" className="w-full relative z-5"
> >
<div className="flex flex-col overflow-auto"> <div className="flex flex-col overflow-auto">
{isLoadingFile ? ( {isLoadingFile ? (
@@ -123,7 +123,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
<FormControl> <FormControl>
<CodeEditor <CodeEditor
lineWrapping lineWrapping
wrapperClassName="h-[35rem] font-mono" wrapperClassName="h-140 font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:
@@ -143,7 +143,7 @@ routers:
</pre> </pre>
<div className="flex justify-end absolute z-50 right-6 top-8"> <div className="flex justify-end absolute z-50 right-6 top-8">
<Button <Button
className="shadow-sm" className="shadow-xs"
variant="secondary" variant="secondary"
type="button" type="button"
onClick={async () => { onClick={async () => {

View File

@@ -97,7 +97,7 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
<> <>
<Tree <Tree
data={directories} data={directories}
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg" className="lg:max-w-76 w-full lg:h-[660px] border rounded-lg"
onSelectChange={(item) => setFile(item?.id || null)} onSelectChange={(item) => setFile(item?.id || null)}
folderIcon={Folder} folderIcon={Folder}
itemIcon={Workflow} itemIcon={Workflow}

View File

@@ -197,7 +197,7 @@ export const ImpersonationBar = () => {
> >
{selectedUser ? ( {selectedUser ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" /> <UserIcon className="mr-2 h-4 w-4 shrink-0" />
<span className="truncate flex flex-col items-start"> <span className="truncate flex flex-col items-start">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{`${selectedUser.name} ${selectedUser.lastName}`.trim() || {`${selectedUser.name} ${selectedUser.lastName}`.trim() ||
@@ -245,7 +245,7 @@ export const ImpersonationBar = () => {
}} }}
> >
<span className="flex items-center gap-2 flex-1"> <span className="flex items-center gap-2 flex-1">
<UserIcon className="h-4 w-4 flex-shrink-0" /> <UserIcon className="h-4 w-4 shrink-0" />
<span className="flex flex-col items-start"> <span className="flex flex-col items-start">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{`${user.name} ${user.lastName}`.trim() || {`${user.name} ${user.lastName}`.trim() ||

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -96,7 +96,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the Libsql database</p> <p>Downloads and sets up the Libsql database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -136,7 +136,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Restart the Libsql service without rebuilding</p> <p>Restart the Libsql service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -176,7 +176,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the Libsql database (requires a previous Start the Libsql database (requires a previous
successful setup) successful setup)
@@ -218,7 +218,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running Libsql database</p> <p>Stop the currently running Libsql database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -243,7 +243,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the Libsql container</p> <p>Open a terminal to the Libsql container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -1,4 +1,3 @@
import { SelectGroup } from "@radix-ui/react-select";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -6,6 +5,7 @@ import { Label } from "@/components/ui/label";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
@@ -28,7 +28,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Sqld Node</Label> <Label>Sqld Node</Label>
@@ -71,7 +71,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Enable Namespaces</Label> <Label>Enable Namespaces</Label>

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -99,7 +99,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the MariaDB database</p> <p>Downloads and sets up the MariaDB database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -141,7 +141,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Restart the MariaDB service without rebuilding</p> <p>Restart the MariaDB service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -183,7 +183,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the MariaDB database (requires a previous Start the MariaDB database (requires a previous
successful setup) successful setup)
@@ -225,7 +225,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running MariaDB database</p> <p>Stop the currently running MariaDB database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -250,7 +250,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the MariaDB container</p> <p>Open a terminal to the MariaDB container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -25,11 +25,11 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -99,7 +99,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the MongoDB database</p> <p>Downloads and sets up the MongoDB database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -139,7 +139,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Restart the MongoDB service without rebuilding</p> <p>Restart the MongoDB service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -179,7 +179,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the MongoDB database (requires a previous Start the MongoDB database (requires a previous
successful setup) successful setup)
@@ -219,7 +219,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running MongoDB database</p> <p>Stop the currently running MongoDB database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -244,7 +244,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the MongoDB container</p> <p>Open a terminal to the MongoDB container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -25,7 +25,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -55,7 +55,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -34,7 +34,7 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => {
})); }));
return ( return (
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full"> <ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
<AreaChart <AreaChart
data={transformedData} data={transformedData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}

View File

@@ -29,7 +29,7 @@ export const DockerCpuChart = ({ accumulativeData }: Props) => {
})); }));
return ( return (
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full"> <ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
<AreaChart <AreaChart
data={transformedData} data={transformedData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}

View File

@@ -29,7 +29,7 @@ export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
})); }));
return ( return (
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full"> <ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
<AreaChart <AreaChart
data={transformedData} data={transformedData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}

View File

@@ -78,7 +78,7 @@ export const DockerDiskUsageChart = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-[16rem]"> <div className="flex items-center justify-center h-64">
<Loader2 className="size-5 animate-spin text-muted-foreground" /> <Loader2 className="size-5 animate-spin text-muted-foreground" />
</div> </div>
); );

View File

@@ -35,7 +35,7 @@ export const DockerMemoryChart = ({
})); }));
return ( return (
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full"> <ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
<AreaChart <AreaChart
data={transformedData} data={transformedData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}

View File

@@ -34,7 +34,7 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => {
})); }));
return ( return (
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full"> <ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
<AreaChart <AreaChart
data={transformedData} data={transformedData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}

View File

@@ -227,7 +227,7 @@ export const ContainerFreeMonitoring = ({
String(currentData.cpu.value ?? "0%").replace("%", ""), String(currentData.cpu.value ?? "0%").replace("%", ""),
10, 10,
)} )}
className="w-[100%]" className="w-full"
/> />
<DockerCpuChart accumulativeData={accumulativeData.cpu} /> <DockerCpuChart accumulativeData={accumulativeData.cpu} />
</div> </div>
@@ -250,7 +250,7 @@ export const ContainerFreeMonitoring = ({
convertMemoryToBytes(currentData.memory.value.total)) * convertMemoryToBytes(currentData.memory.value.total)) *
100 100
} }
className="w-[100%]" className="w-full"
/> />
<DockerMemoryChart <DockerMemoryChart
accumulativeData={accumulativeData.memory} accumulativeData={accumulativeData.memory}
@@ -275,7 +275,7 @@ export const ContainerFreeMonitoring = ({
</span> </span>
<Progress <Progress
value={currentData.disk.value.diskUsedPercentage} value={currentData.disk.value.diskUsedPercentage}
className="w-[100%]" className="w-full"
/> />
<DockerDiskChart <DockerDiskChart
accumulativeData={accumulativeData.disk} accumulativeData={accumulativeData.disk}

View File

@@ -115,7 +115,7 @@ export const ContainerBlockChart = ({ data }: Props) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -84,7 +84,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -99,7 +99,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -122,7 +122,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -71,7 +71,7 @@ export function CPUChart({ data }: CPUChartProps) {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -86,7 +86,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -90,7 +90,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const data = payload?.[0]?.payload; const data = payload?.[0]?.payload;
return ( return (
<div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="rounded-lg border bg-background p-2 shadow-xs">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground"> <span className="text-[0.70rem] uppercase text-muted-foreground">

View File

@@ -202,7 +202,7 @@ export const ShowPaidMonitoring = ({
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border text-card-foreground shadow-sm p-6"> <div className="rounded-lg border text-card-foreground shadow-xs p-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" /> <Clock className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Uptime</h3> <h3 className="text-sm font-medium">Uptime</h3>
@@ -212,7 +212,7 @@ export const ShowPaidMonitoring = ({
</p> </p>
</div> </div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6"> <div className="rounded-lg border text-card-foreground shadow-xs p-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" /> <Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">CPU Usage</h3> <h3 className="text-sm font-medium">CPU Usage</h3>
@@ -220,7 +220,7 @@ export const ShowPaidMonitoring = ({
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p> <p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
</div> </div>
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6"> <div className="rounded-lg border text-card-foreground bg-transparent shadow-xs p-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" /> <MemoryStick className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Memory Usage</h3> <h3 className="text-sm font-medium">Memory Usage</h3>
@@ -230,7 +230,7 @@ export const ShowPaidMonitoring = ({
</p> </p>
</div> </div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6"> <div className="rounded-lg border text-card-foreground shadow-xs p-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" /> <HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Disk Usage</h3> <h3 className="text-sm font-medium">Disk Usage</h3>
@@ -240,7 +240,7 @@ export const ShowPaidMonitoring = ({
</div> </div>
{/* System Information */} {/* System Information */}
<div className="rounded-lg border text-card-foreground shadow-sm p-6"> <div className="rounded-lg border text-card-foreground shadow-xs p-6">
<h3 className="text-lg font-medium mb-4">System Information</h3> <h3 className="text-lg font-medium mb-4">System Information</h3>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <div>

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,7 +97,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the MySQL database</p> <p>Downloads and sets up the MySQL database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -137,7 +137,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Restart the MySQL service without rebuilding</p> <p>Restart the MySQL service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -177,7 +177,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the MySQL database (requires a previous Start the MySQL database (requires a previous
successful setup) successful setup)
@@ -217,7 +217,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running MySQL database</p> <p>Stop the currently running MySQL database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -242,7 +242,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the MySQL container</p> <p>Open a terminal to the MySQL container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -25,11 +25,11 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -93,7 +93,8 @@ export function AddOrganization({ organizationId }: Props) {
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
toast.error( toast.error(
`Failed to ${organizationId ? "update" : "create"} organization`, error?.message ??
`Failed to ${organizationId ? "update" : "create"} organization`,
); );
}); });
}; };

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -99,7 +99,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the PostgreSQL database</p> <p>Downloads and sets up the PostgreSQL database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -139,7 +139,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Restart the PostgreSQL service without rebuilding Restart the PostgreSQL service without rebuilding
</p> </p>
@@ -181,7 +181,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the PostgreSQL database (requires a previous Start the PostgreSQL database (requires a previous
successful setup) successful setup)
@@ -221,7 +221,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Stop the currently running PostgreSQL database Stop the currently running PostgreSQL database
</p> </p>
@@ -248,7 +248,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the PostgreSQL container</p> <p>Open a terminal to the PostgreSQL container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -25,11 +25,11 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input enableCopyButton disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input enableCopyButton disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -57,7 +57,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@@ -180,7 +180,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
</FormLabel> </FormLabel>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
className="z-[999] w-[300px]" className="z-999 w-[300px]"
align="start" align="start"
side="top" side="top"
> >

View File

@@ -191,7 +191,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
</FormLabel> </FormLabel>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
className="z-[999] w-[300px]" className="z-999 w-[300px]"
align="start" align="start"
side="top" side="top"
> >

View File

@@ -62,7 +62,7 @@ const dockerImageDefaultPlaceholder: Record<DbType, string> = {
mariadb: "mariadb:11", mariadb: "mariadb:11",
mysql: "mysql:8", mysql: "mysql:8",
postgres: "postgres:18", postgres: "postgres:18",
redis: "redis:7", redis: "redis:8",
}; };
const databasesUserDefaultPlaceholder: Record< const databasesUserDefaultPlaceholder: Record<
@@ -412,7 +412,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
/> />
<Label <Label
htmlFor={key} htmlFor={key}
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer" className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
> >
{value.icon} {value.icon}
{value.label} {value.label}
@@ -765,7 +765,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
name="replicaSets" name="replicaSets"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm"> <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Use Replica Sets</FormLabel> <FormLabel>Use Replica Sets</FormLabel>
</div> </div>

View File

@@ -231,7 +231,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
</FormLabel> </FormLabel>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
className="z-[999] w-[300px]" className="z-999 w-[300px]"
align="start" align="start"
side="top" side="top"
> >
@@ -386,7 +386,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
{templateInfo.template.domains.map((domain, index) => ( {templateInfo.template.domains.map((domain, index) => (
<div <div
key={index} key={index}
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm" className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
> >
<div className="font-medium"> <div className="font-medium">
{domain.serviceName} {domain.serviceName}

View File

@@ -236,7 +236,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Button <Button
variant="outline" variant="outline"
className={cn( className={cn(
"w-full sm:w-[200px] justify-between !bg-input", "w-full sm:w-[200px] justify-between bg-input!",
)} )}
> >
{isLoadingTags {isLoadingTags
@@ -296,7 +296,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
variant={showBookmarksOnly ? "default" : "outline"} variant={showBookmarksOnly ? "default" : "outline"}
size="icon" size="icon"
onClick={() => setShowBookmarksOnly(!showBookmarksOnly)} onClick={() => setShowBookmarksOnly(!showBookmarksOnly)}
className="h-9 w-9 flex-shrink-0" className="h-9 w-9 shrink-0"
disabled={isLoadingBookmarks} disabled={isLoadingBookmarks}
> >
<Bookmark <Bookmark
@@ -311,7 +311,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
onClick={() => onClick={() =>
setViewMode(viewMode === "detailed" ? "icon" : "detailed") setViewMode(viewMode === "detailed" ? "icon" : "detailed")
} }
className="h-9 w-9 flex-shrink-0" className="h-9 w-9 shrink-0"
> >
{viewMode === "detailed" ? ( {viewMode === "detailed" ? (
<LayoutGrid className="size-4" /> <LayoutGrid className="size-4" />
@@ -398,7 +398,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background" className="h-8 w-8 bg-background/80 backdrop-blur-xs hover:bg-background"
onClick={(e) => handleToggleBookmark(e, template.id)} onClick={(e) => handleToggleBookmark(e, template.id)}
> >
<Bookmark <Bookmark
@@ -451,7 +451,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
{/* Template Content */} {/* Template Content */}
{viewMode === "detailed" && ( {viewMode === "detailed" && (
<ScrollArea className="flex-1 p-6"> <ScrollArea className="min-h-0 flex-1 p-6">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{template?.description} {template?.description}
</div> </div>
@@ -534,7 +534,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
</Label> </Label>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
className="z-[999] w-[300px]" className="z-999 w-[300px]"
align="start" align="start"
side="top" side="top"
> >

View File

@@ -297,7 +297,7 @@ export const AdvancedEnvironmentSelector = ({
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="name">Name</Label> <Label htmlFor="name">Name</Label>
<Input <Input
id="name" id="name"
@@ -306,7 +306,7 @@ export const AdvancedEnvironmentSelector = ({
placeholder="Environment name" placeholder="Environment name"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="description">Description (optional)</Label> <Label htmlFor="description">Description (optional)</Label>
<Textarea <Textarea
id="description" id="description"

View File

@@ -5,7 +5,7 @@ import type { StepProps } from "./step-two";
export const StepThree = ({ templateInfo }: StepProps) => { export const StepThree = ({ templateInfo }: StepProps) => {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-grow"> <div className="grow">
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2> <h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
<div className="space-y-4"> <div className="space-y-4">

View File

@@ -201,7 +201,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
return ( return (
<div className="flex flex-col h-full gap-6"> <div className="flex flex-col h-full gap-6">
<div className="flex-grow overflow-auto pb-8"> <div className="grow overflow-auto pb-8">
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2> <h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
{!selectedVariant && ( {!selectedVariant && (

View File

@@ -47,7 +47,7 @@ interface Details {
envVariables: EnvVariable[]; envVariables: EnvVariable[];
shortDescription: string; shortDescription: string;
domains: Domain[]; domains: Domain[];
configFiles?: Mount[]; configFiles?: Mount[] | null;
} }
interface Mount { interface Mount {
@@ -184,7 +184,7 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
> >
{stepper.all.map((step, index, array) => ( {stepper.all.map((step, index, array) => (
<React.Fragment key={step.id}> <React.Fragment key={step.id}>
<li className="flex items-center gap-4 flex-shrink-0"> <li className="flex items-center gap-4 shrink-0">
<Button <Button
type="button" type="button"
role="tab" role="tab"

View File

@@ -154,7 +154,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
lineWrapping lineWrapping
language="properties" language="properties"
readOnly={!canWrite} readOnly={!canWrite}
wrapperClassName="h-[35rem] font-mono" wrapperClassName="h-140 font-mono"
placeholder={`NODE_ENV=development placeholder={`NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/mydb DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your-api-key-here API_KEY=your-api-key-here

View File

@@ -152,7 +152,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
lineWrapping lineWrapping
language="properties" language="properties"
readOnly={!canWrite} readOnly={!canWrite}
wrapperClassName="h-[35rem] font-mono" wrapperClassName="h-140 font-mono"
placeholder={`NODE_ENV=production placeholder={`NODE_ENV=production
PORT=3000 PORT=3000

View File

@@ -207,7 +207,7 @@ export const ShowProjects = () => {
<Card className="h-full bg-sidebar p-2.5 rounded-xl "> <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6"> <div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0"> <CardHeader className="flex-1 p-0">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" /> <FolderInput className="size-6 text-muted-foreground self-center" />
Projects Projects
@@ -290,7 +290,7 @@ export const ShowProjects = () => {
</span> </span>
</div> </div>
)} )}
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5"> <div className="w-full grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-5">
{filteredProjects?.map((project) => { {filteredProjects?.map((project) => {
const emptyServices = project?.environments const emptyServices = project?.environments
.map( .map(

View File

@@ -1,4 +1,4 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -98,7 +98,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Downloads and sets up the Redis database</p> <p>Downloads and sets up the Redis database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -138,7 +138,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Restart the Redis service without rebuilding</p> <p>Restart the Redis service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -178,7 +178,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p> <p>
Start the Redis database (requires a previous Start the Redis database (requires a previous
successful setup) successful setup)
@@ -218,7 +218,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Stop the currently running Redis database</p> <p>Stop the currently running Redis database</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
@@ -243,7 +243,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-60">
<p>Open a terminal to the Redis container</p> <p>Open a terminal to the Redis container</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>

View File

@@ -25,7 +25,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value="default" /> <Input enableCopyButton disabled value="default" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -53,7 +53,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input enableCopyButton disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

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