From 5f3108b68d4c53473452ac6ab6e5df33d845f652 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:17:16 +0200 Subject: [PATCH] feat(ui): unify resource headings with responsive action menus Add responsive overflow handling and dedicated action menus for application, service, database, and server headings, while improving upgrade and email settings flows and returning 404s for missing deployment environments. --- DESIGN.md | 15 + app/Livewire/Notifications/Email.php | 2 +- .../Project/Application/Deployment/Index.php | 5 +- .../Project/Application/Deployment/Show.php | 5 +- .../Project/Database/Backup/Execution.php | 5 +- .../Project/Database/Backup/Index.php | 5 +- app/Livewire/SettingsEmail.php | 2 +- composer.json | 1 - composer.lock | 387 +----------------- resources/css/app.css | 58 +++ .../applications/advanced.blade.php | 4 + .../components/applications/deploy.blade.php | 79 ++++ .../resource-heading-overflow.blade.php | 149 +++++++ .../components/server/advanced.blade.php | 38 ++ .../components/services/advanced.blade.php | 49 +++ .../components/services/restart.blade.php | 50 +++ .../components/upgrade-progress.blade.php | 6 +- .../livewire/notifications/email.blade.php | 24 +- .../project/application/heading.blade.php | 115 ++---- .../project/database/heading.blade.php | 52 +-- .../project/service/heading.blade.php | 117 ++---- .../views/livewire/server/navbar.blade.php | 82 ++-- .../views/livewire/settings-email.blade.php | 12 +- resources/views/livewire/upgrade.blade.php | 90 ++-- routes/web.php | 8 +- .../AdvancedMenuIconConsistencyTest.php | 1 + .../NotificationAuthorizationTest.php | 32 ++ .../DeploymentShowMissingEnvironmentTest.php | 79 ++++ tests/Feature/ExceptionRendererTest.php | 39 ++ .../NotificationEmailResendLayoutTest.php | 25 ++ tests/Feature/ResourceHeadingOverflowTest.php | 36 ++ .../ResourceHeadingUnifiedNavbarTest.php | 103 ++++- .../Feature/ServerNavbarStatusLayoutTest.php | 31 +- .../ServiceDatabaseVerticalNavigationTest.php | 5 +- tests/Feature/SettingsEmailSmtpSetupTest.php | 137 +++++++ tests/Feature/UpgradeComponentTest.php | 30 ++ tests/Unit/CoolifyUpgradeStatusTest.php | 10 + 37 files changed, 1155 insertions(+), 733 deletions(-) create mode 100644 resources/views/components/applications/advanced.blade.php create mode 100644 resources/views/components/applications/deploy.blade.php create mode 100644 resources/views/components/resource-heading-overflow.blade.php create mode 100644 resources/views/components/server/advanced.blade.php create mode 100644 resources/views/components/services/advanced.blade.php create mode 100644 resources/views/components/services/restart.blade.php create mode 100644 tests/Feature/DeploymentShowMissingEnvironmentTest.php create mode 100644 tests/Feature/ExceptionRendererTest.php create mode 100644 tests/Feature/NotificationEmailResendLayoutTest.php create mode 100644 tests/Feature/ResourceHeadingOverflowTest.php create mode 100644 tests/Feature/SettingsEmailSmtpSetupTest.php diff --git a/DESIGN.md b/DESIGN.md index 72ab85abe..a7b26bb66 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -154,6 +154,21 @@ 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. +Desktop resource lifecycle actions dock in `#resource-action-hud-slot` and +use ``. Show primary actions (Deploy, Redeploy, +Restart, Stop) as sibling header buttons. Collapse that group into an Actions +dropdown only when the remaining top-bar width cannot fit them (breadcrumb +keeps a 200px floor). Infrequent operations live in a separate Advanced +dropdown with the grid icon: force restart / force deploy / force cleanup +on services, and Traefik dashboard / refresh proxy status on servers. Place +Advanced immediately after Links, or first in the action cluster when there +is no Links control. Application Deploy is a dropdown with Deploy and +Deploy (without cache). A running service Restart control is a dropdown with +Restart current version and Pull latest and restart. Mobile +headings keep a full-width Actions dropdown because the desktop HUD is hidden +below `xl`. Do not hide primary actions behind a menu on a wide desktop. Links +stay a separate dropdown because the URL list is unbounded. + 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 diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 2aa09ed8f..962c85c33 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -302,7 +302,7 @@ class Email extends Component $this->resetErrorBag(); $this->validate([ 'resendEnabled' => 'boolean', - 'resendApiKey' => 'required|string', + 'resendApiKey' => $this->resendEnabled ? 'required|string' : 'nullable|string', 'smtpFromAddress' => 'required|email', 'smtpFromName' => 'required|string', ], [ diff --git a/app/Livewire/Project/Application/Deployment/Index.php b/app/Livewire/Project/Application/Deployment/Index.php index cf23e304e..1625e9f08 100644 --- a/app/Livewire/Project/Application/Deployment/Index.php +++ b/app/Livewire/Project/Application/Deployment/Index.php @@ -65,10 +65,11 @@ class Index extends Component if (! $project) { return redirect()->route('dashboard'); } - $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']); + $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first(); if (! $environment) { - return redirect()->route('dashboard'); + abort(404); } + $environment->load(['applications']); $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); if (! $application) { return redirect()->route('dashboard'); diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 9ed0ab807..a1657f55b 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -39,10 +39,11 @@ class Show extends Component if (! $project) { return redirect()->route('dashboard'); } - $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']); + $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first(); if (! $environment) { - return redirect()->route('dashboard'); + abort(404); } + $environment->load(['applications']); $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first(); if (! $application) { return redirect()->route('dashboard'); diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php index 00c177008..a96a190e8 100644 --- a/app/Livewire/Project/Database/Backup/Execution.php +++ b/app/Livewire/Project/Database/Backup/Execution.php @@ -26,10 +26,11 @@ class Execution extends Component if (! $project) { return redirect()->route('dashboard'); } - $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']); + $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first(); if (! $environment) { - return redirect()->route('dashboard'); + abort(404); } + $environment->load(['applications']); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); if (! $database) { return redirect()->route('dashboard'); diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index a8b5b8e5d..72313054d 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -14,10 +14,11 @@ class Index extends Component if (! $project) { return redirect()->route('dashboard'); } - $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']); + $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first(); if (! $environment) { - return redirect()->route('dashboard'); + abort(404); } + $environment->load(['applications']); $database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); if (! $database) { return redirect()->route('dashboard'); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 6b6952316..121fe92a4 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -203,7 +203,7 @@ class SettingsEmail extends Component $this->authorize('update', $this->settings); $this->validate([ 'resendEnabled' => 'boolean', - 'resendApiKey' => 'required|string', + 'resendApiKey' => $this->resendEnabled ? 'required|string' : 'nullable|string', 'smtpFromAddress' => 'required|email', 'smtpFromName' => 'required|string', ], [ diff --git a/composer.json b/composer.json index 8fc57e499..74cf52e27 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,6 @@ "phpstan/phpstan": "^2.2.8", "rector/rector": "^2.6.1", "serversideup/spin": "^3.3.0", - "spatie/laravel-ignition": "^2.12.0", "symfony/http-client": "^7.4.16" }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index 70cda8129..55cafa9d1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37ccd1e33f9eebf1d5b4a88b1113e0b3", + "content-hash": "9cfa94749243cbf2879e01939d7f86cb", "packages": [ { "name": "aws/aws-crt-php", @@ -16541,391 +16541,6 @@ ], "time": "2026-07-16T20:06:00+00:00" }, - { - "name": "spatie/backtrace", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/backtrace.git", - "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc", - "reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "ext-json": "*", - "laravel/serializable-closure": "^1.3 || ^2.0", - "phpunit/phpunit": "^9.3 || ^11.4.3", - "spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6", - "symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Backtrace\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van de Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "A better backtrace", - "homepage": "https://github.com/spatie/backtrace", - "keywords": [ - "Backtrace", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/backtrace/issues", - "source": "https://github.com/spatie/backtrace/tree/1.8.2" - }, - "funding": [ - { - "url": "https://github.com/sponsors/spatie", - "type": "github" - }, - { - "url": "https://spatie.be/open-source/support-us", - "type": "other" - } - ], - "time": "2026-03-11T13:48:28+00:00" - }, - { - "name": "spatie/error-solutions", - "version": "1.1.3", - "source": { - "type": "git", - "url": "https://github.com/spatie/error-solutions.git", - "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", - "reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "illuminate/broadcasting": "^10.0|^11.0|^12.0", - "illuminate/cache": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "livewire/livewire": "^2.11|^3.5.20", - "openai-php/client": "^0.10.1", - "orchestra/testbench": "8.22.3|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "phpstan/phpstan": "^2.1", - "psr/simple-cache": "^3.0", - "psr/simple-cache-implementation": "^3.0", - "spatie/ray": "^1.28", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "vlucas/phpdotenv": "^5.5" - }, - "suggest": { - "openai-php/client": "Require get solutions from OpenAI", - "simple-cache-implementation": "To cache solutions from OpenAI" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Ignition\\": "legacy/ignition", - "Spatie\\ErrorSolutions\\": "src", - "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ruben Van Assche", - "email": "ruben@spatie.be", - "role": "Developer" - } - ], - "description": "This is my package error-solutions", - "homepage": "https://github.com/spatie/error-solutions", - "keywords": [ - "error-solutions", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/error-solutions/issues", - "source": "https://github.com/spatie/error-solutions/tree/1.1.3" - }, - "funding": [ - { - "url": "https://github.com/Spatie", - "type": "github" - } - ], - "time": "2025-02-14T12:29:50+00:00" - }, - { - "name": "spatie/flare-client-php", - "version": "1.11.1", - "source": { - "type": "git", - "url": "https://github.com/spatie/flare-client-php.git", - "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/53f41b08a27cc039e1a8ed2be9a202e924f31bad", - "reference": "53f41b08a27cc039e1a8ed2be9a202e924f31bad", - "shasum": "" - }, - "require": { - "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "php": "^8.0", - "spatie/backtrace": "^1.6.1", - "symfony/http-foundation": "^5.2|^6.0|^7.0|^8.0", - "symfony/mime": "^5.2|^6.0|^7.0|^8.0", - "symfony/process": "^5.2|^6.0|^7.0|^8.0", - "symfony/var-dumper": "^5.2|^6.0|^7.0|^8.0" - }, - "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.5.0", - "pestphp/pest": "^1.20|^2.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "spatie/pest-plugin-snapshots": "^1.0|^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.3.x-dev" - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Spatie\\FlareClient\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Send PHP errors to Flare", - "homepage": "https://github.com/spatie/flare-client-php", - "keywords": [ - "exception", - "flare", - "reporting", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/flare-client-php/issues", - "source": "https://github.com/spatie/flare-client-php/tree/1.11.1" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-05-15T09:31:32+00:00" - }, - { - "name": "spatie/ignition", - "version": "1.16.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/ignition.git", - "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/b59385bb7aa24dae81bcc15850ebecfda7b40838", - "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^8.0", - "spatie/backtrace": "^1.7.1", - "spatie/error-solutions": "^1.1.2", - "spatie/flare-client-php": "^1.9", - "symfony/console": "^5.4.42|^6.0|^7.0|^8.0", - "symfony/http-foundation": "^5.4.42|^6.0|^7.0|^8.0", - "symfony/mime": "^5.4.42|^6.0|^7.0|^8.0", - "symfony/var-dumper": "^5.4.42|^6.0|^7.0|^8.0" - }, - "require-dev": { - "illuminate/cache": "^9.52|^10.0|^11.0|^12.0|^13.0", - "mockery/mockery": "^1.4", - "pestphp/pest": "^1.20|^2.0|^3.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "psr/simple-cache-implementation": "*", - "symfony/cache": "^5.4.38|^6.0|^7.0|^8.0", - "symfony/process": "^5.4.35|^6.0|^7.0|^8.0", - "vlucas/phpdotenv": "^5.5" - }, - "suggest": { - "openai-php/client": "Require get solutions from OpenAI", - "simple-cache-implementation": "To cache solutions from OpenAI" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.5.x-dev" - } - }, - "autoload": { - "psr-4": { - "Spatie\\Ignition\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Spatie", - "email": "info@spatie.be", - "role": "Developer" - } - ], - "description": "A beautiful error page for PHP applications.", - "homepage": "https://flareapp.io/ignition", - "keywords": [ - "error", - "flare", - "laravel", - "page" - ], - "support": { - "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", - "forum": "https://twitter.com/flareappio", - "issues": "https://github.com/spatie/ignition/issues", - "source": "https://github.com/spatie/ignition" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-03-17T10:51:08+00:00" - }, - { - "name": "spatie/laravel-ignition", - "version": "2.12.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "45b3b6e1e73fc161cba2149972698644b99594ee" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/45b3b6e1e73fc161cba2149972698644b99594ee", - "reference": "45b3b6e1e73fc161cba2149972698644b99594ee", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "illuminate/support": "^11.0|^12.0|^13.0", - "nesbot/carbon": "^2.72|^3.0", - "php": "^8.2", - "spatie/ignition": "^1.16", - "symfony/console": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" - }, - "require-dev": { - "livewire/livewire": "^3.7.0|^4.0|dev-josh/v3-laravel-13-support", - "mockery/mockery": "^1.6.12", - "openai-php/client": "^0.10.3|^0.19", - "orchestra/testbench": "^v9.16.0|^10.6|^11.0", - "pestphp/pest": "^3.7|^4.0", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.8", - "vlucas/phpdotenv": "^5.6.2" - }, - "suggest": { - "openai-php/client": "Require get solutions from OpenAI", - "psr/simple-cache-implementation": "Needed to cache solutions from OpenAI" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" - }, - "providers": [ - "Spatie\\LaravelIgnition\\IgnitionServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Spatie\\LaravelIgnition\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Spatie", - "email": "info@spatie.be", - "role": "Developer" - } - ], - "description": "A beautiful error page for Laravel applications.", - "homepage": "https://flareapp.io/ignition", - "keywords": [ - "error", - "flare", - "laravel", - "page" - ], - "support": { - "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", - "forum": "https://twitter.com/flareappio", - "issues": "https://github.com/spatie/laravel-ignition/issues", - "source": "https://github.com/spatie/laravel-ignition" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-03-17T12:20:04+00:00" - }, { "name": "staabm/side-effects-detector", "version": "1.0.5", diff --git a/resources/css/app.css b/resources/css/app.css index 4bd2ceb46..b2dc81941 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -398,6 +398,12 @@ html[data-theme="custom"] .animate-spin { color: var(--theme-bright-color) !important; } +/* Opt out of the brand spinner when the surrounding surface is a selected/neutral control. */ +.dark .animate-spin.spinner-current, +html[data-theme="custom"] .animate-spin.spinner-current { + color: inherit !important; +} + html[data-theme="custom"] #nprogress .bar { background: var(--theme-bright-color) !important; } @@ -1710,6 +1716,58 @@ html[data-theme="custom"] textarea:disabled { overflow: visible; } +.resource-heading-overflow { + display: inline-flex; + align-items: center; +} + +.resource-heading-overflow-items { + display: flex; + align-items: center; + gap: 0.125rem; +} + +.resource-heading-overflow-items.is-measuring { + display: flex !important; + visibility: hidden !important; + position: absolute !important; + inset: auto auto 0 0 !important; + flex-direction: row !important; + width: max-content !important; + height: auto !important; + max-width: none !important; + max-height: none !important; + overflow: visible !important; + pointer-events: none !important; + padding: 0 !important; + border: 0 !important; + background: transparent !important; + box-shadow: none !important; +} + +.resource-heading-overflow-separator { + width: 1px; + align-self: stretch; + margin: 0.25rem 0.25rem; + background: color-mix(in srgb, var(--coollabs-line) 80%, transparent); +} + +.resource-heading-overflow.is-collapsed .resource-heading-overflow-separator { + width: auto; + height: 1px; + align-self: auto; + margin: 0.25rem 0.375rem; +} + +.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > .button, +.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > a.button { + width: 100%; + justify-content: flex-start; + height: auto; + min-height: 2rem; + padding: 0.375rem 0.625rem; +} + /* Heading action group: buttons + dropdown triggers styled like the tabs */ .application-heading-actions .button, .application-heading-actions .app-tab, diff --git a/resources/views/components/applications/advanced.blade.php b/resources/views/components/applications/advanced.blade.php new file mode 100644 index 000000000..8bd169b01 --- /dev/null +++ b/resources/views/components/applications/advanced.blade.php @@ -0,0 +1,4 @@ +{{-- Application Advanced is unused. Deploy (without cache) lives on the Deploy + dropdown. Keep this file so Advanced menus stay on the name="grid" icon. --}} +@props(['application']) + diff --git a/resources/views/components/applications/deploy.blade.php b/resources/views/components/applications/deploy.blade.php new file mode 100644 index 000000000..e5214353f --- /dev/null +++ b/resources/views/components/applications/deploy.blade.php @@ -0,0 +1,79 @@ +@props(['application']) + +@php + $canDeploy = auth()->user()->can('deploy', $application); + $isExited = str($application->status)->startsWith('exited'); + $isSwarm = $application->destination->server->isSwarm(); + $isCompose = $application->build_pack === 'dockercompose'; + $withoutCacheAction = $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)'; +@endphp + +@if ($isExited) + @if ($isSwarm) + + @else +
+ + + +
+ @endif +@elseif (! $isSwarm) +
+ + + +
+@elseif (! $isCompose) + +@endif diff --git a/resources/views/components/resource-heading-overflow.blade.php b/resources/views/components/resource-heading-overflow.blade.php new file mode 100644 index 000000000..44554e899 --- /dev/null +++ b/resources/views/components/resource-heading-overflow.blade.php @@ -0,0 +1,149 @@ +{{-- + Resource header actions: render inline in the top bar, then collapse into + an Actions dropdown when the remaining header width cannot fit them. +--}} +@props([ + 'id' => null, + 'label' => 'Actions', +]) + +
class('resource-heading-overflow relative shrink-0') }} + data-resource-heading-overflow + x-data="{ + collapsed: false, + open: false, + ro: null, + onNavigated: null, + update() { + const needed = this.measureInlineWidth() + const available = this.availableWidth() + const next = needed > 0 && needed > available + if (next === this.collapsed) { + return + } + this.collapsed = next + if (!next) { + this.open = false + } + }, + hudSlot() { + return this.$el.closest('#resource-action-hud-slot') + ?? document.getElementById('resource-action-hud-slot') + }, + headerRow() { + return this.hudSlot()?.parentElement ?? null + }, + measureInlineWidth() { + const items = this.$refs.items + if (!items) { + return 0 + } + items.classList.add('is-measuring') + const width = items.scrollWidth + items.classList.remove('is-measuring') + return width + }, + availableWidth() { + const row = this.headerRow() + const hud = this.hudSlot() + if (!row || !hud) { + return Infinity + } + + let reserved = 0 + for (const child of row.children) { + if (child === hud) { + continue + } + if (child.classList.contains('flex-1') || child.querySelector(':scope .flex-1')) { + reserved += 200 + continue + } + reserved += child.getBoundingClientRect().width + } + + let hudSiblings = 0 + const cluster = this.$el.parentElement + if (cluster) { + for (const child of cluster.children) { + if (child === this.$el) { + continue + } + hudSiblings += child.getBoundingClientRect().width + } + } + + return Math.max(0, row.clientWidth - reserved - hudSiblings - 16) + }, + observe() { + if (typeof ResizeObserver === 'undefined') { + return + } + if (!this.ro) { + this.ro = new ResizeObserver(() => this.update()) + } + const row = this.headerRow() + if (row) { + this.ro.observe(row) + } + this.ro.observe(this.$el) + }, + init() { + this.onNavigated = () => this.$nextTick(() => { + this.observe() + this.update() + }) + document.addEventListener('livewire:navigated', this.onNavigated) + + this.$nextTick(() => { + this.observe() + this.update() + requestAnimationFrame(() => { + this.observe() + this.update() + }) + }) + }, + destroy() { + this.ro?.disconnect() + if (this.onNavigated) { + document.removeEventListener('livewire:navigated', this.onNavigated) + } + }, + }" + x-init="init()" + x-effect="$dispatch('resource-actions-toggled', { open })" + @click.outside="open = false" + @keydown.escape.window="open = false" + @resize.window.debounce.50ms="update()" + :class="{ 'is-collapsed': collapsed }" +> + + +
+ {{ $slot }} +
+
diff --git a/resources/views/components/server/advanced.blade.php b/resources/views/components/server/advanced.blade.php new file mode 100644 index 000000000..63cc23714 --- /dev/null +++ b/resources/views/components/server/advanced.blade.php @@ -0,0 +1,38 @@ +@props([ + 'serverIp', + 'traefikDashboardAvailable' => false, +]) + +
+ + + +
diff --git a/resources/views/components/services/advanced.blade.php b/resources/views/components/services/advanced.blade.php new file mode 100644 index 000000000..3a6377aec --- /dev/null +++ b/resources/views/components/services/advanced.blade.php @@ -0,0 +1,49 @@ +@php + $status = str($service->status ?? ''); + $canDeploy = auth()->user()->can('deploy', $service); + $canStop = auth()->user()->can('stop', $service); + $hasAdvancedItems = ! $status->contains('running'); +@endphp + +@if ($hasAdvancedItems) +
+ + + +
+@endif diff --git a/resources/views/components/services/restart.blade.php b/resources/views/components/services/restart.blade.php new file mode 100644 index 000000000..6acfd2963 --- /dev/null +++ b/resources/views/components/services/restart.blade.php @@ -0,0 +1,50 @@ +@props(['service']) + +@php + $status = str($service->status ?? ''); + $canDeploy = auth()->user()->can('deploy', $service); + $isRunning = $status->contains('running'); + $isDegraded = $status->contains('degraded'); +@endphp + +@if ($isRunning || $isDegraded) + @if ($isRunning) +
+ + + +
+ @else + + @endif +@endif diff --git a/resources/views/components/upgrade-progress.blade.php b/resources/views/components/upgrade-progress.blade.php index f1a39644c..fec9ce2d1 100644 --- a/resources/views/components/upgrade-progress.blade.php +++ b/resources/views/components/upgrade-progress.blade.php @@ -25,14 +25,14 @@
@@ -40,7 +40,7 @@