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 @@