From 62b2fe2330b12f4e72e82a4f20126e98bb9bcb6e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:35:15 +0200 Subject: [PATCH] feat(ui): add volume backup storage controls and grouped navigation --- app/Livewire/Storage/Resources.php | 72 ++++++++++++++ resources/css/utilities.css | 4 +- .../configuration-sidebar.blade.php | 23 +++-- .../database/configuration-sidebar.blade.php | 16 +-- .../views/components/forms/listbox.blade.php | 97 +++++++++++++++---- .../service/configuration-sidebar.blade.php | 15 +-- .../shared-variables/view-controls.blade.php | 6 +- .../views/livewire/project/index.blade.php | 12 +-- .../livewire/project/resource/index.blade.php | 8 +- .../project/service/configuration.blade.php | 21 ++-- .../views/livewire/project/show.blade.php | 12 +-- .../views/livewire/server/index.blade.php | 6 +- .../shared/list-search-controls.blade.php | 6 +- .../livewire/storage/resources.blade.php | 52 +++++++++- resources/views/livewire/tags/show.blade.php | 8 +- .../Feature/HighlightedButtonStylingTest.php | 2 +- .../ResourceHeadingUnifiedNavbarTest.php | 17 ++-- tests/Feature/ServerIndexViewSwitcherTest.php | 8 +- .../ServiceDatabaseVerticalNavigationTest.php | 47 ++++++--- tests/Feature/TeamScopedBackupStorageTest.php | 53 ++++++++++ 20 files changed, 373 insertions(+), 112 deletions(-) diff --git a/app/Livewire/Storage/Resources.php b/app/Livewire/Storage/Resources.php index 4f39943e4..1e9b80c3a 100644 --- a/app/Livewire/Storage/Resources.php +++ b/app/Livewire/Storage/Resources.php @@ -4,6 +4,7 @@ namespace App\Livewire\Storage; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; +use App\Models\ScheduledVolumeBackup; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -15,6 +16,8 @@ class Resources extends Component public array $selectedStorages = []; + public array $selectedVolumeStorages = []; + public function mount(): void { $this->authorize('view', $this->storage); @@ -26,6 +29,13 @@ class Resources extends Component foreach ($backups as $backup) { $this->selectedStorages[$backup->id] = $this->storage->id; } + + ScheduledVolumeBackup::query() + ->where('s3_storage_id', $this->storage->id) + ->where('save_s3', true) + ->each(function (ScheduledVolumeBackup $backup): void { + $this->selectedVolumeStorages[$backup->id] = $this->storage->id; + }); } public function disableS3(int $backupId): void @@ -80,6 +90,61 @@ class Resources extends Component $this->dispatch('success', 'Backup moved.', "Moved to {$newStorage->name}."); } + public function disableVolumeS3(int $backupId): void + { + $this->authorize('update', $this->storage); + + $backup = ScheduledVolumeBackup::query() + ->where('id', $backupId) + ->where('s3_storage_id', $this->storage->id) + ->firstOrFail(); + + $backup->update([ + 'save_s3' => false, + 's3_storage_id' => null, + ]); + + unset($this->selectedVolumeStorages[$backupId]); + + $this->dispatch('success', 'S3 disabled.', 'S3 backup has been disabled for this schedule.'); + } + + public function moveVolumeBackup(int $backupId): void + { + $this->authorize('update', $this->storage); + + $backup = ScheduledVolumeBackup::query() + ->where('id', $backupId) + ->where('s3_storage_id', $this->storage->id) + ->firstOrFail(); + $newStorageId = $this->selectedVolumeStorages[$backupId] ?? null; + + if (! $newStorageId || (int) $newStorageId === $this->storage->id) { + $this->dispatch('error', 'No change.', 'The backup is already using this storage.'); + + return; + } + + $newStorage = S3Storage::query() + ->where('id', $newStorageId) + ->where('team_id', $this->storage->team_id) + ->first(); + + if (! $newStorage) { + $this->dispatch('error', 'Storage not found.'); + + return; + } + + $this->authorize('update', $newStorage); + + $backup->update(['s3_storage_id' => $newStorage->id]); + + unset($this->selectedVolumeStorages[$backupId]); + + $this->dispatch('success', 'Backup moved.', "Moved to {$newStorage->name}."); + } + public function render() { $backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id) @@ -92,8 +157,15 @@ class Resources extends Component ->orderBy('name') ->get(['id', 'name', 'is_usable']); + $volumeBackups = ScheduledVolumeBackup::query() + ->where('s3_storage_id', $this->storage->id) + ->where('save_s3', true) + ->with('backupable.resource') + ->get(); + return view('livewire.storage.resources', [ 'groupedBackups' => $backups, + 'volumeBackups' => $volumeBackups, 'allStorages' => $allStorages, ]); } diff --git a/resources/css/utilities.css b/resources/css/utilities.css index bd69dedee..8be3a06e4 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -131,11 +131,11 @@ } @utility button-highlighted { - @apply border-2 text-coollabs-200 dark:text-white bg-coollabs-50 dark:bg-coollabs/20 border-coollabs dark:border-coollabs-100 hover:bg-coollabs hover:text-white dark:hover:bg-coollabs-100 dark:hover:text-white; + @apply border-coollabs-200 bg-linear-to-b from-coollabs-100 to-coollabs-200 text-white! hover:from-coollabs-100 hover:to-coollabs hover:text-white!; } @utility control-selected { - @apply bg-coollabs text-white dark:bg-coollabs dark:text-white; + @apply bg-linear-to-b from-coollabs-100 to-coollabs-200 text-white; } @utility loading-indicator { diff --git a/resources/views/components/application/configuration-sidebar.blade.php b/resources/views/components/application/configuration-sidebar.blade.php index fe70c8cdb..078f3cafa 100644 --- a/resources/views/components/application/configuration-sidebar.blade.php +++ b/resources/views/components/application/configuration-sidebar.blade.php @@ -52,12 +52,12 @@ 'visible' => ! $application->destination->server->isSwarm() && auth()->user()?->can('canAccessTerminal'), ], [ - 'label' => 'Deployment', + 'label' => 'Deployment Logs', 'route' => 'project.application.deployment.index', 'active' => str($currentRoute)->startsWith('project.application.deployment'), ], [ - 'label' => 'Runtime', + 'label' => 'Runtime Logs', 'route' => 'project.application.logs', 'active' => $currentRoute === 'project.application.logs', ], @@ -142,8 +142,8 @@ 'Persistent Storage' => 'storages', 'Backups' => 'database', 'Terminal' => 'browser-terminal', - 'Deployment' => 'time-back', - 'Runtime' => 'unordered-list', + 'Deployment Logs' => 'time-back', + 'Runtime Logs' => 'unordered-list', 'Git Source' => 'sources', 'Servers' => 'servers', 'Scheduled Tasks' => 'calendar', @@ -160,14 +160,17 @@ // Discord-style groups for the settings sidebar $menuGroups = [ - 'Settings' => ['General', 'Domains', 'Advanced', 'Swarm', 'Environment Variables', 'Persistent Storage', 'Backups'], - 'Build & deploy' => ['Git Source', 'Servers', 'Healthcheck'], - 'Automation' => ['Scheduled Tasks', 'Webhooks', 'Preview Deployments'], - 'Logs' => ['Deployment', 'Runtime'], - 'Operations' => ['Terminal', 'Rollback', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'], + 'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck'], + 'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics'], + 'Deploy' => ['Git Source', 'Servers', 'Preview Deployments'], + 'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups'], + 'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback', 'Tags', 'Danger Zone'], ]; $groupedMenuItems = collect($menuGroups) - ->map(fn (array $labels) => collect($configurationMenuItems)->whereIn('label', $labels)->values()) + ->map(fn (array $labels) => collect($labels) + ->map(fn (string $label) => collect($configurationMenuItems)->firstWhere('label', $label)) + ->filter() + ->values()) ->filter(fn ($items) => $items->isNotEmpty()); // In-page sections (cards) shown as sub-items under the active page diff --git a/resources/views/components/database/configuration-sidebar.blade.php b/resources/views/components/database/configuration-sidebar.blade.php index a2f301230..2c62641f8 100644 --- a/resources/views/components/database/configuration-sidebar.blade.php +++ b/resources/views/components/database/configuration-sidebar.blade.php @@ -14,7 +14,7 @@ ['label' => 'Backups', 'route' => 'project.database.backup.index', 'icon' => 'database', 'visible' => $database->isBackupSolutionAvailable()], ['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'navigate' => false, 'visible' => auth()->user()?->can('update', $database)], ['label' => 'Servers', 'route' => 'project.database.servers', 'icon' => 'servers'], - ['label' => 'Runtime', 'route' => 'project.database.logs', 'icon' => 'unordered-list', 'navigate' => false], + ['label' => 'Runtime Logs', 'route' => 'project.database.logs', 'icon' => 'unordered-list', 'navigate' => false], ['label' => 'Terminal', 'route' => 'project.database.command', 'icon' => 'browser-terminal', 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')], ['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'icon' => 'notifications'], ['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'icon' => 'feedback'], @@ -32,14 +32,18 @@ ]); $menuGroups = [ - 'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Backups', 'Import Backup', 'Servers'], - 'Automation' => ['Webhooks', 'Healthcheck'], - 'Logs' => ['Runtime'], - 'Operations' => ['Terminal', 'Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'], + 'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Healthcheck'], + 'Observe & troubleshoot' => ['Runtime Logs', 'Terminal', 'Metrics'], + 'Deploy' => ['Servers'], + 'Automation' => ['Webhooks', 'Backups', 'Import Backup'], + 'Operations' => ['Resource Operations', 'Resource Limits', 'Tags', 'Danger Zone'], ]; $groupedItems = collect($menuGroups) - ->map(fn (array $labels) => $configurationItems->whereIn('label', $labels)->values()) + ->map(fn (array $labels) => collect($labels) + ->map(fn (string $label) => $configurationItems->firstWhere('label', $label)) + ->filter() + ->values()) ->filter(fn ($items) => $items->isNotEmpty()); $pageSections = $database->type() === 'standalone-postgresql' diff --git a/resources/views/components/forms/listbox.blade.php b/resources/views/components/forms/listbox.blade.php index 0dd64ccf2..4da47bee0 100644 --- a/resources/views/components/forms/listbox.blade.php +++ b/resources/views/components/forms/listbox.blade.php @@ -13,10 +13,12 @@ 'value' => null, // initial value when wire=false 'disabled' => false, 'tooltip' => true, + 'portal' => false, ]) @php $triggerId = ($htmlId ?? $id).'-trigger'; + $panelId = ($htmlId ?? $id).'-panel'; @endphp