diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index a387b6f88..1c1bea2f6 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -131,7 +131,7 @@ public function syncData(bool $toModel = false) $this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3; $this->saveS3 = $this->backup->save_s3; $this->disableLocalBackup = $this->backup->disable_local_backup ?? false; - $this->s3StorageId = $this->backup->s3_storage_id; + $this->s3StorageId = $this->backup->s3_storage_id ?? $this->availableS3StorageIds()->first(); $this->databasesToBackup = $this->backup->databases_to_backup; $this->dumpAll = $this->backup->dump_all; $this->timeout = $this->backup->timeout; @@ -217,6 +217,11 @@ public function instantSave() } } + public function updatedS3StorageId(): void + { + $this->instantSave(); + } + private function customValidate() { if (! is_numeric($this->backup->s3_storage_id)) { @@ -225,17 +230,13 @@ private function customValidate() // S3 backup cannot be enabled without a valid S3 storage owned by the team $availableS3Ids = $this->availableS3StorageIds(); - if ($this->backup->save_s3 && ! $availableS3Ids->contains($this->backup->s3_storage_id)) { - if ($availableS3Ids->isEmpty()) { - $this->backup->s3_storage_id = $this->s3StorageId = null; + if ($availableS3Ids->isEmpty()) { + $this->backup->s3_storage_id = $this->s3StorageId = null; + if ($this->backup->save_s3) { $this->backup->save_s3 = $this->saveS3 = false; - } elseif ($this->backup->s3_storage_id === null && $availableS3Ids->count() === 1) { - $this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first(); - } else { - $this->backup->s3_storage_id = $this->s3StorageId = null; - - throw new Exception('Please select a valid S3 storage to enable S3 backups.'); } + } elseif (! $availableS3Ids->contains($this->backup->s3_storage_id)) { + $this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first(); } // Validate that disable_local_backup can only be true when S3 backup is enabled @@ -254,9 +255,14 @@ private function availableS3StorageIds(): Collection { $storages = collect($this->availableS3Storages); $storageIds = $storages->pluck('id')->filter()->all(); + + if (empty($storageIds)) { + return collect(); + } + $teamIds = $storages->pluck('team_id')->reject(fn ($teamId) => $teamId === null)->unique()->values()->all(); - if (empty($storageIds) || empty($teamIds)) { + if (empty($teamIds)) { return collect(); } diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index a25e0141f..5efc9102b 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -132,10 +132,10 @@ @class([ 'relative h-auto', 'w-full' => $buttonFullWidth, - 'w-auto' => ! $buttonFullWidth, + 'w-full sm:w-auto' => ! $buttonFullWidth, ])> @if (isset($trigger)) -
+
{{ $trigger }}
@elseif ($customButton) diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index edea7b80f..4f810d755 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -1,7 +1,7 @@
-
+

Scheduled Backup

-
+
Save @@ -9,16 +9,19 @@ Backup Now @endif @if ($backup->database_id !== 0) -
- + + shortConfirmationLabel="Database Name"> + + Delete Backups and Schedule + +
@endif
@@ -39,16 +42,27 @@ helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." /> @endif
- @if ($saveS3) -
- - +
+
+ S3 Storage + @if (!$saveS3) + (currently disabled) + @endif + @if ($saveS3) + + @endif +
+ + @if ($availableS3Storages->isEmpty()) + + @else @foreach ($availableS3Storages as $s3) @endforeach - -
- @endif + @endif +
+

Settings

diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index 15d42a7a5..0b7a9724a 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -1,6 +1,6 @@
@isset($backup) -
+

Executions ({{ $executions_count }})

@if ($executions_count > 0)
@@ -21,14 +21,18 @@
@endif -
+
Cleanup Failed Backups - + shortConfirmationLabel="Confirmation"> + + Cleanup Deleted + +
{{ data_get($execution, 'message') }}
@endif -
+
@if (data_get($execution, 'status') === 'success') - Download @endif @php @@ -177,11 +181,15 @@ class="flex flex-col gap-4"> $deleteActions[] = 'This backup execution record will be deleted.'; } @endphp - + shortConfirmationLabel="Backup Filename"> + + Delete + +
@empty diff --git a/tests/Feature/BackupEditValidationTest.php b/tests/Feature/BackupEditValidationTest.php index 4bd39d2c3..fe396b5da 100644 --- a/tests/Feature/BackupEditValidationTest.php +++ b/tests/Feature/BackupEditValidationTest.php @@ -117,8 +117,8 @@ function createS3StorageForBackupEditValidationTest(Team|int $team, string $name expect($backup->s3_storage_id)->toBe($s3->id); }); -it('requires an explicit S3 selection when multiple storages are available', function () { - createS3StorageForBackupEditValidationTest($this->team, 'First S3'); +it('defaults to the first available storage when multiple storages are available', function () { + $firstS3 = createS3StorageForBackupEditValidationTest($this->team, 'First S3'); createS3StorageForBackupEditValidationTest($this->team, 'Second S3'); $backup = createBackupForEditValidationTest($this->team, [ 'save_s3' => false, @@ -126,13 +126,14 @@ function createS3StorageForBackupEditValidationTest(Team|int $team, string $name ]); Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s]) + ->assertSet('s3StorageId', $firstS3->id) ->set('saveS3', true) ->call('instantSave') - ->assertDispatched('error'); + ->assertDispatched('success'); $backup->refresh(); - expect($backup->save_s3)->toBeFalsy(); - expect($backup->s3_storage_id)->toBeNull(); + expect($backup->save_s3)->toBeTruthy(); + expect($backup->s3_storage_id)->toBe($firstS3->id); }); it('accepts the S3 storage scope passed to the component', function () { @@ -152,3 +153,55 @@ function createS3StorageForBackupEditValidationTest(Team|int $team, string $name expect($backup->save_s3)->toBeTruthy(); expect($backup->s3_storage_id)->toBe($s3->id); }); + +it('shows available S3 storages even when S3 backup is disabled', function () { + createS3StorageForBackupEditValidationTest($this->team, 'First S3'); + createS3StorageForBackupEditValidationTest($this->team, 'Second S3'); + $backup = createBackupForEditValidationTest($this->team, [ + 'save_s3' => false, + 's3_storage_id' => null, + ]); + + Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s]) + ->assertSee('First S3') + ->assertSee('Second S3'); +}); + +it('shows disabled S3 storage dropdown when no storages are available', function () { + $backup = createBackupForEditValidationTest($this->team, [ + 'save_s3' => false, + 's3_storage_id' => null, + ]); + + Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s]) + ->assertSee('No S3 storage available'); +}); + +it('shows when S3 backups are currently disabled', function () { + createS3StorageForBackupEditValidationTest($this->team); + $backup = createBackupForEditValidationTest($this->team, [ + 'save_s3' => false, + 's3_storage_id' => null, + ]); + + Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s]) + ->assertSee('S3 Storage') + ->assertSee('(currently disabled)'); +}); + +it('saves selected S3 storage immediately when it changes', function () { + createS3StorageForBackupEditValidationTest($this->team, 'First S3'); + $secondS3 = createS3StorageForBackupEditValidationTest($this->team, 'Second S3'); + $backup = createBackupForEditValidationTest($this->team, [ + 'save_s3' => false, + 's3_storage_id' => null, + ]); + + Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->team->s3s]) + ->set('s3StorageId', $secondS3->id) + ->assertDispatched('success'); + + $backup->refresh(); + expect($backup->save_s3)->toBeFalsy(); + expect($backup->s3_storage_id)->toBe($secondS3->id); +});