feat(ui): move maintenance mode to its own page with a confirmed toggle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-11-06 15:20:41 +01:00
co-authored by Claude Opus 5.5
parent 53e21abe97
commit 17a9fa624e
8 changed files with 47 additions and 15 deletions
+6 -2
View File
@@ -38,13 +38,17 @@ class Maintenance extends Component
&& in_array($server->proxyType(), [ProxyTypes::TRAEFIK->value, ProxyTypes::CADDY->value], true));
}
public function instantSaveMaintenance(): void
/**
* Turn maintenance mode on or off. The header button calls this after the user confirms.
*/
public function toggleMaintenance(): void
{
try {
$this->authorize('update', $this->resource);
$this->resource->is_maintenance_enabled = $this->isMaintenanceEnabled;
$this->resource->is_maintenance_enabled = ! $this->resource->is_maintenance_enabled;
$this->resource->save();
$this->resource->syncMaintenancePage();
$this->isMaintenanceEnabled = (bool) $this->resource->is_maintenance_enabled;
$this->dispatch('success', $this->isMaintenanceEnabled ? 'Maintenance mode enabled.' : 'Maintenance mode disabled.');
} catch (\Throwable $e) {
$this->isMaintenanceEnabled = (bool) $this->resource->fresh()?->is_maintenance_enabled;
@@ -119,6 +119,11 @@
'route' => 'project.application.resource-operations',
'active' => $currentRoute === 'project.application.resource-operations',
],
[
'label' => 'Maintenance Mode',
'route' => 'project.application.maintenance',
'active' => $currentRoute === 'project.application.maintenance',
],
[
'label' => 'Metrics',
'route' => 'project.application.metrics',
@@ -175,6 +180,7 @@
'Rollback' => 'time-back',
'Resource Limits' => 'cpu',
'Resource Operations' => 'server-update',
'Maintenance Mode' => 'bandage',
'Metrics' => 'graph',
'Analytics' => 'analytics',
'Security' => 'shield-alert',
@@ -188,7 +194,7 @@
'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Files', 'Metrics', 'Analytics', 'Security'],
'Deploy' => ['Git Source', 'Servers', 'Preview Deployments'],
'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups'],
'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback', 'Tags'],
'Operations' => ['Resource Operations', 'Maintenance Mode', 'Resource Limits', 'Rollback', 'Tags'],
'Danger Zone' => ['Danger Zone'],
];
$groupedMenuItems = collect($menuGroups)
@@ -22,6 +22,7 @@
['label' => 'Scheduled Tasks', 'route' => 'project.service.scheduled-tasks.show', 'icon' => 'calendar'],
['label' => 'Webhooks', 'route' => 'project.service.webhooks', 'icon' => 'webhook'],
['label' => 'Resource Operations', 'route' => 'project.service.resource-operations', 'icon' => 'server-update'],
['label' => 'Maintenance Mode', 'route' => 'project.service.maintenance', 'icon' => 'bandage'],
['label' => 'Security', 'route' => 'project.service.vulnerabilities', 'icon' => 'shield-alert', 'visible' => $imageScanningAllowed,
'security' => $imageScanningAllowed ? \App\Services\ImageScan\SecurityStatus::stored($service)->status->value : null],
['label' => 'Tags', 'route' => 'project.service.tags', 'icon' => 'tags'],
@@ -40,7 +41,7 @@
'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage'],
'Observe & troubleshoot' => ['Runtime Logs', 'Terminal', 'Files', 'Analytics', 'Security'],
'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups'],
'Operations' => ['Resource Operations', 'Tags'],
'Operations' => ['Resource Operations', 'Maintenance Mode', 'Tags'],
'Danger Zone' => ['Danger Zone'],
];
@@ -14,6 +14,7 @@
<livewire:project.application.general :application="$application" />
@elseif ($currentRoute === 'project.application.domains')
<livewire:project.application.domains :application="$application" />
@elseif ($currentRoute === 'project.application.maintenance')
<livewire:project.shared.maintenance :resource="$application" />
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
<livewire:project.application.swarm :application="$application" />
@@ -81,6 +81,7 @@
:lazy="(bool) ($service->server ?? $service->destination?->server)?->isTrafficAnalyticsEnabled()" />
@elseif ($currentRoute === 'project.service.domains')
<livewire:project.service.domains :service="$service" />
@elseif ($currentRoute === 'project.service.maintenance')
<livewire:project.shared.maintenance :resource="$service" />
@elseif ($currentRoute === 'project.service.environment-variables')
<livewire:project.shared.environment-variable.all :resource="$service" />
@@ -3,14 +3,33 @@
<x-application.settings-section id="maintenance-section" title="Maintenance mode"
helper="Show a maintenance page with a 503 status on all domains of this resource. The containers keep running, and the change applies without a redeploy.">
@if ($this->isSupported)
<x-slot:badges>
{{-- The inline wrapper puts the badge text on the same line as the title text. --}}
<span>
@if ($isMaintenanceEnabled)
<x-status-badge label="On" type="success" />
@else
<x-status-badge label="Off" type="neutral" />
@endif
</span>
</x-slot:badges>
@can('update', $resource)
<x-slot:actions>
<x-modal-confirmation wire:key="maintenance-toggle-{{ $isMaintenanceEnabled ? 'on' : 'off' }}"
:ignoreWire="false" submitAction="toggleMaintenance" :confirmWithText="false"
:confirmWithPassword="false" :isErrorButton="! $isMaintenanceEnabled"
:title="$isMaintenanceEnabled ? 'Disable maintenance mode?' : 'Enable maintenance mode?'"
:buttonTitle="$isMaintenanceEnabled ? 'Disable maintenance' : 'Enable maintenance'"
:step2ButtonText="$isMaintenanceEnabled ? 'Disable maintenance' : 'Enable maintenance'"
:warningMessage="$isMaintenanceEnabled
? 'Visitors can reach this resource again right away.'
: 'Visitors cannot reach this resource until you disable maintenance mode.'"
:actions="$isMaintenanceEnabled
? ['All domains of this resource serve the resource again.']
: ['All domains of this resource show the maintenance page with a 503 status.', 'The containers keep running.']" />
</x-slot:actions>
@endcan
<div class="flex flex-col gap-4">
<div class="grid gap-4 lg:grid-cols-2">
<x-forms.listbox canGate="update" :canResource="$resource" id="isMaintenanceEnabled"
label="Maintenance mode" onChange="instantSaveMaintenance" :options="[
['value' => false, 'label' => 'Off: serve the resource'],
['value' => true, 'label' => 'On: show the maintenance page'],
]" />
</div>
<x-forms.textarea canGate="update" :canResource="$resource" id="maintenancePage" rows="10" monospace
label="Custom maintenance page (HTML)"
placeholder="Leave empty to use the Coolify default maintenance page."
+2
View File
@@ -424,6 +424,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::prefix('project/{project_uuid}/environment/{environment_uuid}/application/{application_uuid}')->group(function () {
Route::get('/', ApplicationConfiguration::class)->name('project.application.configuration');
Route::get('/domains', ApplicationConfiguration::class)->name('project.application.domains');
Route::get('/maintenance', ApplicationConfiguration::class)->name('project.application.maintenance');
Route::get('/swarm', ApplicationConfiguration::class)->name('project.application.swarm');
Route::get('/advanced', ApplicationConfiguration::class)->name('project.application.advanced');
Route::get('/environment-variables', ApplicationConfiguration::class)->name('project.application.environment-variables');
@@ -490,6 +491,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::prefix('project/{project_uuid}/environment/{environment_uuid}/service/{service_uuid}')->group(function () {
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
Route::get('/domains', ServiceConfiguration::class)->name('project.service.domains');
Route::get('/maintenance', ServiceConfiguration::class)->name('project.service.maintenance');
Route::get('/logs', Logs::class)->name('project.service.logs');
Route::get('/analytics', ServiceConfiguration::class)->name('project.service.analytics');
Route::get('/vulnerabilities', ServiceConfiguration::class)->name('project.service.vulnerabilities');
+2 -4
View File
@@ -262,8 +262,7 @@ it('lets an admin enable maintenance and save a custom page', function () {
->set('maintenancePage', '<h1>Back soon</h1>')
->call('submit')
->assertDispatched('success')
->set('isMaintenanceEnabled', true)
->call('instantSaveMaintenance')
->call('toggleMaintenance')
->assertDispatched('success');
$application->refresh();
@@ -292,8 +291,7 @@ it('does not let a member or another team change maintenance mode', function (st
session(['currentTeam' => $team]);
Livewire::test(Maintenance::class, ['resource' => $service])
->set('isMaintenanceEnabled', true)
->call('instantSaveMaintenance')
->call('toggleMaintenance')
->assertDispatched('error')
->set('maintenancePage', '<h1>Hacked</h1>')
->call('submit')