diff --git a/.env.development.example b/.env.development.example index 0454dd44a..29162a59a 100644 --- a/.env.development.example +++ b/.env.development.example @@ -29,11 +29,12 @@ DB_PORT=5432 # DB_WRITE_PASSWORD= # DB_STICKY=true -# Enable Laravel Telescope for debugging -TELESCOPE_ENABLED=false -# Enable Laravel Debugbar (disabled by default; set true when needed) -DEBUGBAR_ENABLED=false + +# Server-Timing headers + on-screen HUD (defaults ON when APP_ENV=local). +# Force on in any environment (including production): SERVER_TIMING_ENABLED=true +# Force off even in local: SERVER_TIMING_ENABLED=false +# SERVER_TIMING_ENABLED=true # Vite dev server. Defaults to localhost. For phone/LAN/Tailscale access, set to # the host machine's reachable IP (e.g. VITE_HOST=100.75.155.70), then recreate vite. diff --git a/.env.testing b/.env.testing index d72330b5d..48d893596 100644 --- a/.env.testing +++ b/.env.testing @@ -9,7 +9,6 @@ CACHE_DRIVER=array SESSION_DRIVER=array QUEUE_CONNECTION=sync MAIL_MAILER=array -TELESCOPE_ENABLED=false REDIS_HOST=127.0.0.1 diff --git a/.github/workflows/coolify-sha-build.yml b/.github/workflows/coolify-sha-build.yml index 0a5315a49..8c653e1d1 100644 --- a/.github/workflows/coolify-sha-build.yml +++ b/.github/workflows/coolify-sha-build.yml @@ -76,15 +76,35 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + env: + REGISTRY: ${{ env.GITHUB_REGISTRY }} + BRANCH: ${{ github.ref_name }} + SHA: ${{ github.sha }} run: | + IMAGE="${REGISTRY}/${IMAGE_NAME}" + TAG_ARGS=(--tag "${IMAGE}:sha-${SHA}") + # Moving tag for the latest production-line SHA image (v4.x only). + if [ "${BRANCH}" = "v4.x" ]; then + TAG_ARGS+=(--tag "${IMAGE}:edge") + fi docker buildx imagetools create \ - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-amd64 \ - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-aarch64 \ - --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + "${IMAGE}:sha-${SHA}-amd64" \ + "${IMAGE}:sha-${SHA}-aarch64" \ + "${TAG_ARGS[@]}" - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + env: + REGISTRY: ${{ env.DOCKER_REGISTRY }} + BRANCH: ${{ github.ref_name }} + SHA: ${{ github.sha }} run: | + IMAGE="${REGISTRY}/${IMAGE_NAME}" + TAG_ARGS=(--tag "${IMAGE}:sha-${SHA}") + # Moving tag for the latest production-line SHA image (v4.x only). + if [ "${BRANCH}" = "v4.x" ]; then + TAG_ARGS+=(--tag "${IMAGE}:edge") + fi docker buildx imagetools create \ - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-amd64 \ - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-aarch64 \ - --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + "${IMAGE}:sha-${SHA}-amd64" \ + "${IMAGE}:sha-${SHA}-aarch64" \ + "${TAG_ARGS[@]}" diff --git a/.gitignore b/.gitignore index 401adc775..086b946d0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ CHANGELOG.md /.workspaces tests/Browser/Screenshots tests/v4/Browser/Screenshots +ref # Local generated Lima configs .dev/bin/ diff --git a/AGENTS.md b/AGENTS.md index b4faa103a..566d79777 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,7 +167,6 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/boost (BOOST) - v2 - laravel/dusk (DUSK) - v8 - laravel/pint (PINT) - v1 -- laravel/telescope (TELESCOPE) - v5 - pestphp/pest (PEST) - v4 - phpunit/phpunit (PHPUNIT) - v12 - rector/rector (RECTOR) - v2 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 33d438b91..910657159 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -140,13 +140,19 @@ After installing Docker (or Orbstack) and Spin, verify the installation: |------|-----|------| | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | | Mailpit (email catcher) | `http://localhost:8025` | | - | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | -> [!NOTE] -> To enable Telescope, add the following to your `.env` file: -> ```env -> TELESCOPE_ENABLED=true -> ``` + **Server-Timing + HUD** (headers + bottom-right pill on full HTML pages): + + | Setting | Effect | + |---------|--------| + | `APP_ENV=local` and `SERVER_TIMING_ENABLED` unset | **On** (default in dev) | + | `SERVER_TIMING_ENABLED=true` | **On** in any env, including production | + | `SERVER_TIMING_ENABLED=false` | **Off** even when `APP_ENV=local` | + + Metrics: `app` / `db` / `php` / `dbslow` (ms), `queries`, `html` (bytes), `mem` (MB). + HUD keeps a request log (click row → AI-ready dump). Production: enable only + temporarily (`SERVER_TIMING_ENABLED=true`); if you use `config:cache`, rebuild + or clear config after changing the env var. ## Development Notes diff --git a/RELEASE.md b/RELEASE.md index 0f956ebfd..e83609ad3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -20,7 +20,7 @@ Coolify uses two long-lived branches so production fixes can ship without waitin | Branch | Role | Docker image tags | How it ships | | --- | --- | --- | --- | -| **`v4.x`** | Production / releasable line | `sha-` via **Build Coolify (SHA)** | GitHub release promotes the SHA image to a semantic version (and `latest` for stable releases) | +| **`v4.x`** | Production / releasable line | `sha-` and moving `edge` via **Build Coolify (SHA)** | GitHub release promotes the SHA image to a semantic version (and `latest` for stable releases) | | **`next`** | Development line for features and larger changes | Branch tag (for example `next`) via **Staging Build** | Becomes production only after merge into `v4.x` | ### Where to merge @@ -59,6 +59,7 @@ Only commits on **`v4.x`** produce production SHA images and can be tagged for a - Merge the release commit into `v4.x` through a pull request. - The `Build Coolify (SHA)` workflow builds AMD64 and ARM64 images and publishes them to Docker Hub and GHCR using immutable architecture tags. - After both builds complete, the workflow creates the multi-architecture `sha-` manifest in both registries. + - For pushes to **`v4.x`**, the same multi-architecture manifest is also tagged as `edge`, so `coollabsio/coolify:edge` always points at the latest production-line SHA image. Builds from `main` publish only the immutable `sha-` tags. - This workflow does not update a semantic version tag or `latest`. 3. **Wait for the SHA Image** diff --git a/UI_REDESIGN.md b/UI_REDESIGN.md new file mode 100644 index 000000000..194d7f0c3 --- /dev/null +++ b/UI_REDESIGN.md @@ -0,0 +1,640 @@ +# Coolify UI redesign + +This branch restyles Coolify without changing its Livewire + Blade + Alpine + +Tailwind v4 architecture. The visual system now covers the global shell, +project and environment pages, application navigation, settings surfaces, +tables, modals, toasts, terminals, and metrics. + +Use this file as the source of truth when updating another page. The older +Graphite-only notes are no longer accurate. + +Onboarding validation and live server validation checkpoints share +`` (idle / pending / running / success / error) inside a +compact divided list, not legacy green check SVGs or fixed-width status rows. + +> **Maintainer rules** +> +> - Keep the work frontend-focused unless existing data must be exposed to the +> view. +> - Preserve routes, Livewire bindings, permissions, confirmations, and working +> interactions while changing layout and presentation. +> - Do not write or run tests for this redesign branch. +> - Validate Blade with `docker exec coolify php artisan view:cache`, then clear +> it with `docker exec coolify php artisan view:clear`. +> - Build frontend assets in the Vitee container with +> `docker exec coolify-vite npm run build`. +> - Use existing components before adding another styling abstraction. + +--- + +## 1. Visual direction + +The interface is compact and product-focused: + +- near-neutral layered surfaces instead of large bordered boxes; +- 13–14px UI typography and 32px controls; +- hairline rings instead of heavy borders; +- full-width data tables for dense collections; +- outline Reicon glyphs through ``; +- the Coolify purple brand accent in light mode; +- the readable Coolify yellow accent in dark mode; +- subtle active-item gradients that fade completely into the surrounding + background at the far edge, with the active pill rounded only on the left + (square on the right so it bleeds into the content edge); +- sentence-case labels and headings; +- never use the em dash (`—`) in UI copy. Prefer a period, colon, comma, or + ASCII hyphen (`-`) for empty cells and separators. + +Avoid oversized titles, generic dashboard cards, strong shadows, thick +dividers, native browser selects, and isolated colored buttons that do not +match the current action styles. + +--- + +## 2. Development and cascade notes + +PHP runs in the `coolify` container. The development app is normally available +at `http://localhost:8000`, with Vite on port `5173`. + +`resources/css/app.css` still contains unlayered global element rules for +headings, labels, and tables. Tailwind utilities are layered, so the +unlayered rules can win unexpectedly. + +The settings and dense-surface CSS therefore lives as plain unlayered CSS near +the end of `resources/css/app.css`, beginning at: + +```css +/* Coollabs layer-card settings surfaces */ +``` + +Important consequences: + +- scope restyled forms with `.application-settings-form` or + `.application-settings-workspace`; +- add shared surface overrides to the unlayered block instead of stacking + `!important` utilities; +- listbox panels require ancestors with `overflow: visible`; +- anchored cards use `scroll-margin-top: 7rem` to clear both fixed navigation + layers; +- modal shells reuse the layer-card classes but keep content-width sizing on + desktop; +- Alpine code inside quoted Blade attributes must not introduce conflicting + quote characters. + +--- + +## 3. Tokens and color behavior + +The surface ladder is defined in `resources/css/app.css`. + +| Token | Light | Dark | Use | +|---|---|---|---| +| `--coollabs-canvas` | near white | 10% neutral | page canvas | +| `--coollabs-elevated` | 98% neutral | 15% neutral | shells and card headers | +| `--coollabs-base` | white | 17% neutral | nested card bodies | +| `--coollabs-recessed` | 96% neutral | 20% neutral | inputs and listboxes | +| `--coollabs-fill` | 92.2% neutral | 26.9% neutral | dividers and passive fills | +| `--coollabs-line` | translucent dark | 32% neutral | control borders | +| `--coollabs-hairline` | 93.5% neutral | 26.9% neutral | shell rings | +| `--coollabs-subtle` | 55.6% neutral | 70.8% neutral | labels and muted titles | + +Accent behavior is intentionally theme-aware: + +- **Light mode:** Coolify purple (`coollabs`) for active controls, focus, + primary actions, and navigation accents. +- **Dark mode:** Coolify yellow (`warning`) for the same states because the + original purple did not provide sufficient text and ring contrast. + +Do not hard-code blue focus rings or leave yellow accent utilities active in +light mode. Primary action patterns should normally follow: + +```html +bg-coollabs/10 text-coollabs ring-coollabs/25 +dark:bg-warning/15 dark:text-warning dark:ring-warning/25 +``` + +The filled top-level action/tab treatment uses the same palette at a restrained +opacity rather than a fully saturated fill. + +--- + +## 4. Page shells and navigation + +### Global shell + +- Main sidebar groups are compact, use outline Reicons, and keep a 32px row + height. +- Active sidebar rows have a curved accent rail and a subtle horizontal + gradient. The gradient must fade to the exact sidebar background at the + right edge in both themes. +- Nested items use a thin guide line with a visible active segment, not a thick + box border. +- The update badge sits on the version row and uses a tiny fully rounded + primary-action pill. + +### Layer-2 navigation + +Application and server pages use the same fixed second navigation layer +directly below the global topbar. Do not keep a large in-flow resource heading +or legacy `.navbar-main` tabs on one resource type while using the compact +layer-2 bar on another. Active tabs are a light brand fill: + +- purple tint in light mode; +- yellow tint in dark mode; +- no fully saturated tab background. + +Keep route-derived active state in Blade/Livewire. Do not rely only on Alpine +state because it can disappear after polling or a Livewire morph. + +The global topbar owns the current resource identity and its compact status +badges. Layer 2 owns route tabs, resource links, and contextual action buttons +only. If a resource is missing from `x-top-breadcrumb`, extend the global +topbar instead of repeating its name or status summary in layer 2. Mobile +resource navigation may repeat this context because the desktop global topbar +is hidden there. + +Only add layer-2 tabs when they represent real sibling routes inside one +context. Never repeat main-sidebar destinations such as Dashboard, Projects, +Terminal, Servers, Sources, Destinations, or Storage as a second tab row. A +single collection page does not need a tab just to fill the bar; keep its +primary action in the page header instead. When tabs are useful, their left edge +uses the same compact `pl-2` alignment as application navigation rather than +the content container's wide horizontal padding. + +The dashboard is a compact overview, not a metrics wall. Use two full-width +sections that follow the projects-page grid pattern: projects first, then +servers. Keep one `New` action in the page header and let its modal choose the +resource type. Place active deployments above the resource grids as a compact, +live-updating table rather than a metric card. Communicate server health with +the shared status badge. + +### Top-level dashboard destinations + +Every page opened directly from the main sidebar uses the same compact content +shell: + +- 24px page title and a 13px muted summary; +- the primary action at the top right using the restrained brand fill; +- no legacy `coolbox`, `.navbar-main`, or oversized subtitle block; +- four-column compact cards for small browsable collections; +- a dense table instead of cards when the collection is expected to grow; +- `x-empty` anatomy for empty states; +- `x-status-badge` for state and `x-reicon` for all interface icons. + +Collection cards are `min-h-28` or `min-h-32`, use a 32px icon tile, and keep +secondary metadata at 11px. They must not grow into dashboard-sized summary +cards. Sources, destinations, S3 storage, private keys, and shared-variable +scopes use this pattern. + +Top-level settings families such as Team, Notifications, Keys & Tokens, and +instance Settings use a compact header followed by a small route-derived tab +strip. The active tab uses the same purple-light/yellow-dark tint as resource +tabs. Do not nest ` + + +
+

+ Link this server to its {{ $providerLabel }} resource for provider status and power controls. +

+ + + +
+ + +
+ +
+ + or + +
+ + + + @if ($searchError) + {{ $searchError }} + @elseif ($noMatch) + + Try another token, confirm the resource ID, or verify the server IP. + + @elseif ($matched) +
+
+
+

+ {{ $matched['name'] ?? 'Matched resource' }} +

+

+ {{ $matched['id'] ?? 'Unknown ID' }} + @if ($matched['status'] ?? null) + · + {{ ucfirst($matched['status']) }} + @endif +

+
+ +
+ +
+ @endif +
+ diff --git a/resources/views/components/server/provider-token-picker.blade.php b/resources/views/components/server/provider-token-picker.blade.php new file mode 100644 index 000000000..3cf124550 --- /dev/null +++ b/resources/views/components/server/provider-token-picker.blade.php @@ -0,0 +1,52 @@ +@props([ + 'provider', + 'providerLabel', + 'routeType' => null, + 'tokens', +]) + + + @if ($tokens->isEmpty()) + + + + + + + + + + + @else + + @endif + diff --git a/resources/views/components/server/sidebar-proxy.blade.php b/resources/views/components/server/sidebar-proxy.blade.php index dba5d7db5..9461753d6 100644 --- a/resources/views/components/server/sidebar-proxy.blade.php +++ b/resources/views/components/server/sidebar-proxy.blade.php @@ -1,139 +1,49 @@ @php - $serverPageItems = [ - [ - 'label' => 'Configuration', - 'route' => 'server.show', - 'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'), - ], - [ - 'label' => 'Proxy', - 'route' => 'server.proxy', - 'active' => request()->routeIs('server.proxy', 'server.proxy.*'), - 'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server, - ], - [ - 'label' => 'Sentinel', - 'route' => 'server.sentinel', - 'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'), - 'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server), - ], - [ - 'label' => 'Resources', - 'route' => 'server.resources', - 'active' => request()->routeIs('server.resources'), - ], - [ - 'label' => 'Terminal', - 'route' => 'server.command', - 'active' => request()->routeIs('server.command'), - 'navigate' => false, - 'visible' => auth()->user()?->can('canAccessTerminal'), - ], - [ - 'label' => 'Security', - 'route' => 'server.security.patches', - 'active' => request()->routeIs('server.security.patches'), - 'visible' => auth()->user()?->can('update', $server), - ], - ]; $proxyMenuItems = [ [ 'label' => 'Configuration', 'route' => 'server.proxy', 'active' => request()->routeIs('server.proxy'), + 'icon' => 'settings', ], [ 'label' => 'Dynamic Configurations', 'route' => 'server.proxy.dynamic-confs', 'active' => request()->routeIs('server.proxy.dynamic-confs'), + 'icon' => 'sliders', 'visible' => $server->proxySet(), ], [ 'label' => 'Logs', 'route' => 'server.proxy.logs', 'active' => request()->routeIs('server.proxy.logs'), + 'icon' => 'file-content', 'visible' => $server->proxySet(), 'navigate' => false, ], ]; - $serverPageItems = array_values(array_filter( - $serverPageItems, - fn (array $item): bool => $item['visible'] ?? true, - )); $proxyMenuItems = array_values(array_filter( $proxyMenuItems, fn (array $item): bool => $item['visible'] ?? true, )); - $activeProxyMenuItem = collect($proxyMenuItems)->firstWhere('active', true) ?? $proxyMenuItems[0]; - $activeProxyMenuValue = (($activeProxyMenuItem['navigate'] ?? true) ? 'navigate' : 'location').'|proxy|'.route($activeProxyMenuItem['route'], $parameters); @endphp -
-
- - -
- - -
+ + diff --git a/resources/views/components/server/sidebar-security.blade.php b/resources/views/components/server/sidebar-security.blade.php index 1c30b0ad3..8238b5495 100644 --- a/resources/views/components/server/sidebar-security.blade.php +++ b/resources/views/components/server/sidebar-security.blade.php @@ -1,127 +1,36 @@ @php - $serverPageItems = [ - [ - 'label' => 'Configuration', - 'route' => 'server.show', - 'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'), - ], - [ - 'label' => 'Proxy', - 'route' => 'server.proxy', - 'active' => request()->routeIs('server.proxy', 'server.proxy.*'), - 'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server, - ], - [ - 'label' => 'Sentinel', - 'route' => 'server.sentinel', - 'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'), - 'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server), - ], - [ - 'label' => 'Resources', - 'route' => 'server.resources', - 'active' => request()->routeIs('server.resources'), - ], - [ - 'label' => 'Terminal', - 'route' => 'server.command', - 'active' => request()->routeIs('server.command'), - 'navigate' => false, - 'visible' => auth()->user()?->can('canAccessTerminal'), - ], - [ - 'label' => 'Security', - 'route' => 'server.security.patches', - 'active' => request()->routeIs('server.security.patches'), - 'visible' => auth()->user()?->can('update', $server), - ], - ]; $securityMenuItems = [ [ 'label' => 'Server Patching', 'route' => 'server.security.patches', 'active' => request()->routeIs('server.security.patches'), + 'icon' => 'bandage', ], [ 'label' => 'Terminal Access', 'route' => 'server.security.terminal-access', 'active' => request()->routeIs('server.security.terminal-access'), + 'icon' => 'browser-terminal', 'navigate' => false, ], ]; - $serverPageItems = array_values(array_filter( - $serverPageItems, - fn (array $item): bool => $item['visible'] ?? true, - )); - $activeSecurityMenuItem = collect($securityMenuItems)->firstWhere('active', true) ?? $securityMenuItems[0]; - $activeSecurityMenuValue = (($activeSecurityMenuItem['navigate'] ?? true) ? 'navigate' : 'location').'|security|'.route($activeSecurityMenuItem['route'], $parameters); @endphp -
-
- - -
- - -
+ + diff --git a/resources/views/components/server/sidebar-sentinel.blade.php b/resources/views/components/server/sidebar-sentinel.blade.php index 40962f202..5eee93eba 100644 --- a/resources/views/components/server/sidebar-sentinel.blade.php +++ b/resources/views/components/server/sidebar-sentinel.blade.php @@ -1,121 +1,37 @@ @php - $serverPageItems = [ - [ - 'label' => 'Configuration', - 'route' => 'server.show', - 'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'), - ], - [ - 'label' => 'Proxy', - 'route' => 'server.proxy', - 'active' => request()->routeIs('server.proxy', 'server.proxy.*'), - 'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server, - ], - [ - 'label' => 'Sentinel', - 'route' => 'server.sentinel', - 'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'), - 'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server), - ], - [ - 'label' => 'Resources', - 'route' => 'server.resources', - 'active' => request()->routeIs('server.resources'), - ], - [ - 'label' => 'Terminal', - 'route' => 'server.command', - 'active' => request()->routeIs('server.command'), - 'navigate' => false, - 'visible' => auth()->user()?->can('canAccessTerminal'), - ], - [ - 'label' => 'Security', - 'route' => 'server.security.patches', - 'active' => request()->routeIs('server.security.patches'), - 'visible' => auth()->user()?->can('update', $server), - ], - ]; $sentinelMenuItems = [ [ 'label' => 'Configuration', 'route' => 'server.sentinel', 'active' => request()->routeIs('server.sentinel'), + 'icon' => 'settings', ], [ 'label' => 'Logs', 'route' => 'server.sentinel.logs', 'active' => request()->routeIs('server.sentinel.logs'), + 'icon' => 'file-content', ], ]; - $serverPageItems = array_values(array_filter( - $serverPageItems, - fn (array $item): bool => $item['visible'] ?? true, - )); - $activeSentinelMenuItem = collect($sentinelMenuItems)->firstWhere('active', true) ?? $sentinelMenuItems[0]; - $activeSentinelMenuValue = 'navigate|sentinel|'.route($activeSentinelMenuItem['route'], $parameters); @endphp -
+
+ diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 68ea08d31..3be9db092 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -1,201 +1,130 @@ @php $serverRouteParameters = ['server_uuid' => $server->uuid]; - $serverPageItems = [ - [ - 'label' => 'Configuration', - 'route' => 'server.show', - 'active' => request()->routeIs('server.show', 'server.advanced', 'server.private-key', 'server.cloud-provider-token', 'server.ca-certificate', 'server.cloudflare-tunnel', 'server.docker-cleanup', 'server.destinations', 'server.log-drains', 'server.metrics', 'server.swarm', 'server.delete'), - ], - [ - 'label' => 'Proxy', - 'route' => 'server.proxy', - 'active' => request()->routeIs('server.proxy', 'server.proxy.*'), - 'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server, - ], - [ - 'label' => 'Sentinel', - 'route' => 'server.sentinel', - 'active' => request()->routeIs('server.sentinel', 'server.sentinel.*'), - 'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server), - ], - [ - 'label' => 'Resources', - 'route' => 'server.resources', - 'active' => request()->routeIs('server.resources'), - ], - [ - 'label' => 'Terminal', - 'route' => 'server.command', - 'active' => request()->routeIs('server.command'), - 'navigate' => false, - 'visible' => auth()->user()?->can('canAccessTerminal'), - ], - [ - 'label' => 'Security', - 'route' => 'server.security.patches', - 'active' => request()->routeIs('server.security.patches'), - 'visible' => auth()->user()?->can('update', $server), - ], - ]; $serverMenuItems = [ [ 'label' => 'General', 'route' => 'server.show', 'active' => $activeMenu === 'general', + 'icon' => 'settings', + 'group' => 'Settings', ], [ 'label' => 'Advanced', 'route' => 'server.advanced', 'active' => $activeMenu === 'advanced', + 'icon' => 'grid', + 'group' => 'Settings', 'visible' => $server->isFunctional(), ], [ 'label' => 'Private Key', 'route' => 'server.private-key', 'active' => $activeMenu === 'private-key', + 'icon' => 'keys', + 'group' => 'Settings', ], [ 'label' => 'Cloud Token', 'route' => 'server.cloud-provider-token', 'active' => $activeMenu === 'cloud-provider-token', + 'icon' => 'subscription', + 'group' => 'Settings', 'visible' => (bool) ($server->hetzner_server_id || $server->vultr_instance_id), ], [ 'label' => 'CA Certificate', 'route' => 'server.ca-certificate', 'active' => $activeMenu === 'ca-certificate', + 'icon' => 'file', + 'group' => 'Settings', ], [ 'label' => 'Cloudflare Tunnel', 'route' => 'server.cloudflare-tunnel', 'active' => $activeMenu === 'cloudflare-tunnel', + 'icon' => 'globe', + 'group' => 'Networking', 'visible' => ! $server->isLocalhost(), ], - [ - 'label' => 'Docker Cleanup', - 'route' => 'server.docker-cleanup', - 'active' => $activeMenu === 'docker-cleanup', - 'visible' => $server->isFunctional(), - ], [ 'label' => 'Destinations', 'route' => 'server.destinations', 'active' => $activeMenu === 'destinations', - 'visible' => $server->isFunctional(), - ], - [ - 'label' => 'Log Drains', - 'route' => 'server.log-drains', - 'active' => $activeMenu === 'log-drains', - 'visible' => $server->isFunctional(), - ], - [ - 'label' => 'Metrics', - 'route' => 'server.metrics', - 'active' => $activeMenu === 'metrics', + 'icon' => 'destinations', + 'group' => 'Networking', 'visible' => $server->isFunctional(), ], [ 'label' => 'Swarm', 'route' => 'server.swarm', 'active' => $activeMenu === 'swarm', + 'icon' => 'layers', + 'group' => 'Networking', 'visible' => ! $server->isBuildServer() && ! $server->settings->is_cloudflare_tunnel, ], + [ + 'label' => 'Docker Cleanup', + 'route' => 'server.docker-cleanup', + 'active' => $activeMenu === 'docker-cleanup', + 'icon' => 'storages', + 'group' => 'Operations', + 'visible' => $server->isFunctional(), + ], + [ + 'label' => 'Log Drains', + 'route' => 'server.log-drains', + 'active' => $activeMenu === 'log-drains', + 'icon' => 'notifications', + 'group' => 'Operations', + 'visible' => $server->isFunctional(), + ], + [ + 'label' => 'Metrics', + 'route' => 'server.metrics', + 'active' => $activeMenu === 'metrics', + 'icon' => 'graph', + 'group' => 'Operations', + 'visible' => $server->isFunctional(), + ], [ 'label' => 'Danger', 'route' => 'server.delete', 'active' => $activeMenu === 'danger', - 'visible' => ! $server->isLocalhost(), + 'icon' => 'shield-alert', + 'group' => 'Danger zone', + // Coolify host (id 0) cannot be deleted. Other servers may still use + // host.docker.internal (e.g. Lima VMs) and must keep the Danger menu. + 'visible' => ! $server->is_coolify_host, ], ]; - $serverPageItems = array_values(array_filter( - $serverPageItems, - fn (array $item): bool => $item['visible'] ?? true, - )); - $serverMenuItems = array_values(array_filter( - $serverMenuItems, - fn (array $item): bool => $item['visible'] ?? true, - )); - $activeServerMenuItem = collect($serverMenuItems)->firstWhere('active', true); - $activeServerPageItem = collect($serverPageItems)->firstWhere('active', true); - $activeServerMobileItem = $activeServerMenuItem ?? $activeServerPageItem ?? $serverMenuItems[0]; - $activeServerMobileGroup = $activeServerMenuItem ? 'configuration' : 'server'; - $activeServerMobileNavigation = ($activeServerMobileItem['navigate'] ?? true) ? 'navigate' : 'location'; - $activeServerMenuValue = $activeServerMobileNavigation.'|'.$activeServerMobileGroup.'|'.route($activeServerMobileItem['route'], $serverRouteParameters); + $serverMenuItems = collect($serverMenuItems) + ->filter(fn (array $item): bool => $item['visible'] ?? true) + ->values(); + $groupedServerMenuItems = $serverMenuItems->groupBy('group'); @endphp -
-
- - -
- - -
+ + diff --git a/resources/views/components/service-database/sidebar.blade.php b/resources/views/components/service-database/sidebar.blade.php index 9be795305..890c13fe4 100644 --- a/resources/views/components/service-database/sidebar.blade.php +++ b/resources/views/components/service-database/sidebar.blade.php @@ -4,26 +4,60 @@ 'isImportSupported' => false, ]) - +@php + $items = [ + [ + 'label' => 'General', + 'route' => 'project.service.index', + 'icon' => 'settings', + 'active' => request()->routeIs('project.service.index'), + ], + [ + 'label' => 'Advanced', + 'route' => 'project.service.index.advanced', + 'icon' => 'grid', + 'active' => request()->routeIs('project.service.index.advanced'), + ], + [ + 'label' => 'Backups', + 'route' => 'project.service.database.backups', + 'icon' => 'storages', + 'active' => request()->routeIs('project.service.database.backup*'), + 'visible' => $serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated, + ], + [ + 'label' => 'Import Backup', + 'route' => 'project.service.database.import', + 'icon' => 'upload', + 'active' => request()->routeIs('project.service.database.import'), + 'visible' => $isImportSupported, + 'navigate' => false, + ], + ]; + + $items = array_values(array_filter($items, fn (array $item): bool => $item['visible'] ?? true)); +@endphp + + diff --git a/resources/views/components/services/advanced.blade.php b/resources/views/components/services/advanced.blade.php index 1974bf221..dd2b10b7e 100644 --- a/resources/views/components/services/advanced.blade.php +++ b/resources/views/components/services/advanced.blade.php @@ -1,48 +1,51 @@ - - +@php + $status = str($service->status ?? ''); + $canDeploy = auth()->user()->can('deploy', $service); + $canStop = auth()->user()->can('stop', $service); +@endphp + +
+ + + +
diff --git a/resources/views/components/services/links.blade.php b/resources/views/components/services/links.blade.php index 26df8a685..7740b3eab 100644 --- a/resources/views/components/services/links.blade.php +++ b/resources/views/components/services/links.blade.php @@ -1,12 +1,26 @@ +@php + $linkItemClasses = + 'flex items-center gap-2 px-3 h-8 text-[13px] transition-colors text-neutral-600 dark:text-fg-dim hover:bg-neutral-100 dark:hover:bg-white/[0.06] hover:text-black dark:hover:text-fg'; +@endphp + @if ($links->count() > 0) - - +
+ +
+ @foreach ($links as $link) + + + {{ $link }} + + @endforeach +
+
@endif diff --git a/resources/views/components/settings/navbar.blade.php b/resources/views/components/settings/navbar.blade.php index 565e485d0..fefd36717 100644 --- a/resources/views/components/settings/navbar.blade.php +++ b/resources/views/components/settings/navbar.blade.php @@ -1,29 +1,20 @@ -
-

Settings

-
Instance wide settings for Coolify.
- +@props([ + 'title' => 'Settings', + 'subtitle' => 'Instance configuration and maintenance', +]) + +{{-- Same 1180px shell as the workspace. Title hidden only at xl+ (full desktop). --}} +
+ + @isset($titleActions) + + {{ $titleActions }} + + @endisset + @isset($actions) + + {{ $actions }} + + @endisset +
diff --git a/resources/views/components/settings/sidebar.blade.php b/resources/views/components/settings/sidebar.blade.php index 0a68a9155..be17ffee3 100644 --- a/resources/views/components/settings/sidebar.blade.php +++ b/resources/views/components/settings/sidebar.blade.php @@ -1,8 +1,39 @@ - +@php + $settingsMenuItems = [ + [ + 'label' => 'General', + 'route' => 'settings.index', + 'active' => $activeMenu === 'general', + 'icon' => 'settings', + ], + [ + 'label' => 'Advanced', + 'route' => 'settings.advanced', + 'active' => $activeMenu === 'advanced', + 'icon' => 'grid', + ], + [ + 'label' => 'Updates', + 'route' => 'settings.updates', + 'active' => $activeMenu === 'updates', + 'icon' => 'refresh3', + ], + ]; +@endphp + + diff --git a/resources/views/components/shared-variables/editor.blade.php b/resources/views/components/shared-variables/editor.blade.php new file mode 100644 index 000000000..93959581f --- /dev/null +++ b/resources/views/components/shared-variables/editor.blade.php @@ -0,0 +1,137 @@ +@props([ + 'resource', + 'variables', + 'type', + 'title', + 'view', + 'variablesLabel', +]) + +@php + $alphabeticalPositions = $variables->sortBy('key')->values()->pluck('id')->flip(); + $sharedVariableRows = $variables + ->map(fn($variable) => [ + 'key' => mb_strtolower($variable->key), + 'comment' => mb_strtolower($variable->comment ?? ''), + 'scope' => mb_strtolower(str($type)->headline()), + ]) + ->values(); +@endphp + +
+ + + + + {{ $view === 'normal' ? 'Developer view' : 'Normal view' }} + {{ $view === 'normal' ? 'Developer' : 'Normal' }} + + + + + + @if ($view === 'normal') +
+
+ + + +
+ +
+
+ +
+ + +
+
+ + @can('update', $resource) + + + + + + + @endcan +
+
+ + @if ($variables->isEmpty()) + + @else +
+
+ Name + Scope + Comment + Multiline + +
+ @foreach ($variables as $env) + + @endforeach +
+ +
+
+ @endif + @else +
+ + + + @endif +
+
diff --git a/resources/views/components/slide-over.blade.php b/resources/views/components/slide-over.blade.php index 114135463..90e568ae1 100644 --- a/resources/views/components/slide-over.blade.php +++ b/resources/views/components/slide-over.blade.php @@ -1,4 +1,7 @@ @props(['closeWithX' => false, 'fullScreen' => false]) +{{-- `contents` so event-only shells (no trigger in the default slot) do not leave a + ghost layout box above layer-2 tabs. Trigger buttons in the slot still flow + into the parent as if unwrapped. --}}
merge(['class' => 'relative w-auto h-auto']) }}> +{{ $attributes->merge(['class' => 'contents']) }}> {{ $slot }}