From c0b75d04a652983cffc1b28f3584d3fc74ab84a3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:12:56 +0200 Subject: [PATCH] feat(service): unify database and storage backups on one page List service database schedules with volume backups, add a database picker when creating scheduled DB backups from the service page, and cover the unified table and create flow in tests. --- .../Database/CreateScheduledBackup.php | 87 +++++++++--- .../Project/Service/VolumeBackup/Index.php | 20 ++- resources/css/app.css | 5 + .../create-scheduled-backup.blade.php | 7 + .../service/volume-backup/index.blade.php | 129 ++++++++++++++---- .../CreateScheduledBackupValidationTest.php | 30 ++++ .../ServiceDatabaseVerticalNavigationTest.php | 26 +++- tests/Feature/ServiceResourceRoutingTest.php | 35 +++++ 8 files changed, 288 insertions(+), 51 deletions(-) diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index e72277e80..b4236b215 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; +use App\Models\Service; use App\Models\ServiceDatabase; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; @@ -17,16 +18,36 @@ class CreateScheduledBackup extends Component public $frequency; #[Locked] - public $database; + public $database = null; + + #[Locked] + public ?Service $service = null; + + public ?string $selectedDatabaseUuid = null; public bool $enabled = true; + public function mount(): void + { + if ($this->service) { + $this->authorize('view', $this->service); + $this->selectedDatabaseUuid = $this->availableDatabases()->first()?->uuid; + } + } + public function submit() { try { - $this->authorize('manageBackups', $this->database); + $database = $this->selectedDatabase(); + if (! $database) { + $this->addError('selectedDatabaseUuid', 'Select a database owned by this service.'); - if (! $this->database->isBackupSolutionAvailable()) { + return; + } + + $this->authorize('manageBackups', $database); + + if (! $database->isBackupSolutionAvailable()) { $this->dispatch('error', 'Scheduled backups are not supported for this database type.'); return; @@ -46,36 +67,36 @@ class CreateScheduledBackup extends Component 'frequency' => $this->frequency, 'save_s3' => false, 's3_storage_id' => null, - 'database_id' => $this->database->id, - 'database_type' => $this->database->getMorphClass(), + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), 'team_id' => currentTeam()->id, ]; - if ($this->database->type() === 'standalone-postgresql') { - $payload['databases_to_backup'] = $this->database->postgres_db; - } elseif ($this->database->type() === 'standalone-mysql') { - $payload['databases_to_backup'] = $this->database->mysql_database; - } elseif ($this->database->type() === 'standalone-mariadb') { - $payload['databases_to_backup'] = $this->database->mariadb_database; - } elseif ($this->database->type() === 'standalone-clickhouse') { - $payload['databases_to_backup'] = $this->database->clickhouse_db; + if ($database->type() === 'standalone-postgresql') { + $payload['databases_to_backup'] = $database->postgres_db; + } elseif ($database->type() === 'standalone-mysql') { + $payload['databases_to_backup'] = $database->mysql_database; + } elseif ($database->type() === 'standalone-mariadb') { + $payload['databases_to_backup'] = $database->mariadb_database; + } elseif ($database->type() === 'standalone-clickhouse') { + $payload['databases_to_backup'] = $database->clickhouse_db; } $databaseBackup = ScheduledDatabaseBackup::create($payload); - if ($this->database->getMorphClass() === ServiceDatabase::class) { - $service = $this->database->service; + if ($database->getMorphClass() === ServiceDatabase::class) { + $service = $database->service; $this->redirectRoute('project.service.database.backup.show', [ 'project_uuid' => $service->project()->uuid, 'environment_uuid' => $service->environment->uuid, 'service_uuid' => $service->uuid, - 'stack_service_uuid' => $this->database->uuid, + 'stack_service_uuid' => $database->uuid, 'backup_uuid' => $databaseBackup->uuid, ], navigate: true); } else { $this->redirectRoute('project.database.backup.execution', [ - 'project_uuid' => $this->database->project()->uuid, - 'environment_uuid' => $this->database->environment->uuid, - 'database_uuid' => $this->database->uuid, + 'project_uuid' => $database->project()->uuid, + 'environment_uuid' => $database->environment->uuid, + 'database_uuid' => $database->uuid, 'backup_uuid' => $databaseBackup->uuid, ], navigate: true); } @@ -85,4 +106,32 @@ class CreateScheduledBackup extends Component $this->frequency = ''; } } + + public function render() + { + return view('livewire.project.database.create-scheduled-backup', [ + 'databaseOptions' => $this->availableDatabases()->map(fn (ServiceDatabase $database): array => [ + 'value' => $database->uuid, + 'label' => $database->human_name ?: $database->name, + ])->values()->all(), + ]); + } + + private function availableDatabases() + { + if (! $this->service) { + return collect(); + } + + return $this->service->databases->filter(fn (ServiceDatabase $database): bool => $database->isBackupSolutionAvailable()); + } + + private function selectedDatabase() + { + if (! $this->service) { + return $this->database; + } + + return $this->availableDatabases()->firstWhere('uuid', $this->selectedDatabaseUuid); + } } diff --git a/app/Livewire/Project/Service/VolumeBackup/Index.php b/app/Livewire/Project/Service/VolumeBackup/Index.php index a56653597..49da9e21f 100644 --- a/app/Livewire/Project/Service/VolumeBackup/Index.php +++ b/app/Livewire/Project/Service/VolumeBackup/Index.php @@ -2,8 +2,10 @@ namespace App\Livewire\Project\Service\VolumeBackup; +use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledVolumeBackup; use App\Models\Service; +use App\Models\ServiceDatabase; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -30,6 +32,18 @@ class Index extends Component public function render(): View { + $databaseTargets = $this->service->databases->filter( + fn (ServiceDatabase $database): bool => $database->isBackupSolutionAvailable(), + ); + + $databaseBackups = ScheduledDatabaseBackup::query() + ->with(['database', 'latest_log', 's3']) + ->withCount('executions') + ->where('database_type', (new ServiceDatabase)->getMorphClass()) + ->whereHasMorph('database', [ServiceDatabase::class], fn ($query) => $query->where('service_id', $this->service->id)) + ->latest() + ->get(); + $backups = ScheduledVolumeBackup::query() ->with(['backupable.resource', 'latestExecution', 's3']) ->withCount('executions') @@ -37,7 +51,11 @@ class Index extends Component ->latest() ->get(); - return view('livewire.project.service.volume-backup.index', ['backups' => $backups]); + return view('livewire.project.service.volume-backup.index', [ + 'backups' => $backups, + 'databaseBackups' => $databaseBackups, + 'databaseTargets' => $databaseTargets, + ]); } private function findService(): Service diff --git a/resources/css/app.css b/resources/css/app.css index ccdde81b5..2a9356412 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2238,6 +2238,11 @@ input[type="search"]::-webkit-search-results-decoration { min-width: 50rem; } +.service-backup-table-grid { + grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem 6.5rem minmax(8rem, 1fr); + min-width: 45rem; +} + /* Persistent storage volumes: Name | Source | Destination | [PR suffix] | Backup | [Actions] */ .volumes-table-grid-readonly { grid-template-columns: minmax(12rem, 1.5fr) minmax(8rem, 1fr) minmax(8rem, 1fr) 5rem; diff --git a/resources/views/livewire/project/database/create-scheduled-backup.blade.php b/resources/views/livewire/project/database/create-scheduled-backup.blade.php index 843343b01..682725b03 100644 --- a/resources/views/livewire/project/database/create-scheduled-backup.blade.php +++ b/resources/views/livewire/project/database/create-scheduled-backup.blade.php @@ -1,4 +1,11 @@