From 3115980c7782e581a2458de7ba7874dbfe04311d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:38:35 +0100 Subject: [PATCH] fix(ui): copy service backup path and show S3 badge only for runs stored in S3 Co-Authored-By: Claude Opus 5.5 --- .../Project/Service/BackupExecutions.php | 26 +++++--------- .../service/backup-executions.blade.php | 17 ++++++--- .../ResourceTeamAfterTeamSwitchTest.php | 3 +- tests/Feature/ServiceResourceRoutingTest.php | 35 ++++++++----------- 4 files changed, 37 insertions(+), 44 deletions(-) diff --git a/app/Livewire/Project/Service/BackupExecutions.php b/app/Livewire/Project/Service/BackupExecutions.php index 34be4b11c9..2fcc321703 100644 --- a/app/Livewire/Project/Service/BackupExecutions.php +++ b/app/Livewire/Project/Service/BackupExecutions.php @@ -179,11 +179,11 @@ class BackupExecutions extends Component private function formatExecutions(Collection $rows): Collection { $databaseExecutions = ScheduledDatabaseBackupExecution::query() - ->with(['scheduledDatabaseBackup.database', 'scheduledDatabaseBackup.s3', 'scheduledDatabaseBackup.s3Storages', 's3Replicas']) + ->with(['scheduledDatabaseBackup.database', 's3Replicas.s3']) ->whereIn('id', $rows->where('type', 'database')->pluck('id')) ->get()->keyBy('id'); $volumeExecutions = ScheduledVolumeBackupExecution::query() - ->with(['scheduledVolumeBackup.backupable.resource', 's3', 's3Replicas']) + ->with(['scheduledVolumeBackup.backupable.resource', 's3Replicas.s3']) ->whereIn('id', $rows->where('type', 'storage')->pluck('id')) ->get()->keyBy('id'); @@ -193,21 +193,11 @@ class BackupExecutions extends Component $isDatabase = $row->type === 'database'; $execution = $isDatabase ? $databaseExecutions->get($row->id) : $volumeExecutions->get($row->id); $schedule = $isDatabase ? $execution->scheduledDatabaseBackup : $execution->scheduledVolumeBackup; - $storages = match (true) { - ! $isDatabase => collect([$execution->s3]), - ! $schedule->save_s3 => collect(), - $schedule->s3Storages->isNotEmpty() => $schedule->s3Storages, - default => collect([$schedule->s3]), - }; - $storageLabel = $storages - ->filter(fn ($storage) => $storage?->team_id === $serviceTeamId) - ->map(fn ($storage) => $storage->name.' (bucket: '.$storage->bucket.')') - ->implode(', ') ?: 'Unavailable'; - if ($isDatabase && ! $schedule->save_s3) { - $storageLabel = 'Not configured'; - } elseif (! $isDatabase && ! $execution->s3_storage_id && ! $execution->s3_uploaded && ! $execution->s3_storage_deleted) { - $storageLabel = 'No destination recorded'; - } + $s3Storages = $execution->s3Replicas + ->map(fn ($replica) => $replica->s3?->team_id === $serviceTeamId + ? $replica->s3->name.' (bucket: '.$replica->s3->bucket.')' + : 'Unavailable') + ->implode(', '); return [ 'id' => $row->type.':'.$execution->id, @@ -215,7 +205,7 @@ class BackupExecutions extends Component 'target' => $isDatabase ? ($schedule->database->human_name ?: $schedule->database->name) : $schedule->targetName(), 'type' => $isDatabase ? 'Database' : $schedule->targetType(), 'schedule' => $schedule->frequency, - 's3_tooltip' => ($isDatabase ? 'Current schedule S3 storage: ' : 'S3 storage: ').$storageLabel, + 's3_tooltip' => $s3Storages === '' ? null : 'S3 storage: '.$s3Storages, 'status' => $execution->status, 'started_at' => $execution->created_at, 'size' => $execution->size, diff --git a/resources/views/livewire/project/service/backup-executions.blade.php b/resources/views/livewire/project/service/backup-executions.blade.php index f0c7d99ca7..a12f074662 100644 --- a/resources/views/livewire/project/service/backup-executions.blade.php +++ b/resources/views/livewire/project/service/backup-executions.blade.php @@ -19,7 +19,13 @@

Size

{{ $selectedExecution['size'] ? formatBytes($selectedExecution['size']) : '-' }}

@if ($selectedExecution['filename']) -

Backup path

{{ $selectedExecution['filename'] }}
+
+

Backup path

+
+ {{ $selectedExecution['filename'] }} + +
+
@endif @if ($selectedExecution['message'])

Output

{{ $selectedExecution['message'] }}
@@ -66,10 +72,11 @@ class="data-table-row service-backup-table-grid cursor-pointer text-left text-[13px] text-neutral-700 dark:text-fg-dim"> {{ $execution['target'] }} - - + @if ($execution['s3_tooltip']) + S3 + @endif {{ $execution['type'] }}{{ $execution['schedule'] }} diff --git a/tests/Feature/ResourceTeamAfterTeamSwitchTest.php b/tests/Feature/ResourceTeamAfterTeamSwitchTest.php index dfa4f32181..e6ecae1ffa 100644 --- a/tests/Feature/ResourceTeamAfterTeamSwitchTest.php +++ b/tests/Feature/ResourceTeamAfterTeamSwitchTest.php @@ -189,7 +189,8 @@ test('service backup executions label the S3 storage of the service team after a 'database_id' => $this->serviceDatabase->id, 'database_type' => $this->serviceDatabase->getMorphClass(), ]); - ScheduledDatabaseBackupExecution::create(['scheduled_database_backup_id' => $schedule->id, 'status' => 'success']); + ScheduledDatabaseBackupExecution::create(['scheduled_database_backup_id' => $schedule->id, 'status' => 'success']) + ->s3Replicas()->create(['s3_storage_id' => $ownS3->id, 's3_uploaded' => true]); $component = Livewire::test(BackupExecutions::class, ['service' => $this->service]); session(['currentTeam' => $this->teamB]); diff --git a/tests/Feature/ServiceResourceRoutingTest.php b/tests/Feature/ServiceResourceRoutingTest.php index 3dc6be20d6..93844a8145 100644 --- a/tests/Feature/ServiceResourceRoutingTest.php +++ b/tests/Feature/ServiceResourceRoutingTest.php @@ -635,7 +635,7 @@ test('service backup lists identify the configured S3 storage without extra colu } }); -test('execution tooltips distinguish current database storage from the recorded storage destination', function () { +test('execution tooltips show the S3 storage of each run and no icon for local-only runs', function () { $original = S3Storage::create(['key' => 'key', 'secret' => 'secret', 'region' => 'auto', 'endpoint' => 'https://s3.example.com', 'team_id' => $this->teamA->id, 'name' => 'Cloudflare R2', 'bucket' => 'original']); $current = S3Storage::create(['key' => 'key', 'secret' => 'secret', 'region' => 'auto', 'endpoint' => 'https://s3.example.com', 'team_id' => $this->teamA->id, 'name' => 'Railway S3', 'bucket' => 'current']); $databaseSchedule = ScheduledDatabaseBackup::create([ @@ -646,7 +646,9 @@ test('execution tooltips distinguish current database storage from the recorded 'save_s3' => true, 's3_storage_id' => $current->id, ]); - $databaseSchedule->executions()->create(['status' => 'success', 's3_uploaded' => true]); + $databaseSchedule->executions()->create(['status' => 'success', 's3_uploaded' => true]) + ->s3Replicas()->create(['s3_storage_id' => $original->id, 's3_uploaded' => true]); + $databaseSchedule->executions()->create(['status' => 'success']); $volume = LocalPersistentVolume::create([ 'name' => 'service-data', 'mount_path' => '/data', @@ -659,24 +661,17 @@ test('execution tooltips distinguish current database storage from the recorded 'save_s3' => true, 's3_storage_id' => $current->id, ]); - $volumeSchedule->executions()->create(['status' => 'success', 's3_uploaded' => true, 's3_storage_id' => $original->id]); + $volumeSchedule->executions()->create(['status' => 'success', 's3_uploaded' => true, 's3_storage_id' => $original->id]) + ->s3Replicas()->create(['s3_storage_id' => $original->id, 's3_uploaded' => true]); + $volumeSchedule->executions()->create(['status' => 'success']); - Livewire::test(BackupExecutions::class, ['service' => $this->ownService]) - ->assertSeeHtml('data-tooltip="Current schedule S3 storage: Railway S3 (bucket: current)"') - ->assertSeeHtml('data-tooltip="S3 storage: Cloudflare R2 (bucket: original)"'); + $html = Livewire::test(BackupExecutions::class, ['service' => $this->ownService])->html(); + expect(substr_count($html, 'data-tooltip="S3 storage: Cloudflare R2 (bucket: original)"'))->toBe(2) + ->and(substr_count($html, 'data-tooltip="S3 storage:'))->toBe(2) + ->and($html)->not->toContain('Railway S3'); - $current->update(['team_id' => $this->teamB->id]); - Livewire::test(ServiceVolumeBackupIndex::class, ['service' => $this->ownService]) - ->assertDontSee('Railway S3') - ->assertSeeHtml('data-tooltip="S3 storage: Unavailable"'); - Livewire::test(BackupExecutions::class, ['service' => $this->ownService]) - ->assertDontSee('Railway S3') - ->assertSeeHtml('data-tooltip="Current schedule S3 storage: Unavailable"'); - - $databaseSchedule->update(['save_s3' => false]); - $original->delete(); - Livewire::test(BackupExecutions::class, ['service' => $this->ownService]) - ->assertSeeHtml('data-tooltip="Current schedule S3 storage: Not configured"') - ->assertDontSeeHtml('data-tooltip="S3 storage: Cloudflare R2 (bucket: original)"') - ->assertSeeHtml('data-tooltip="S3 storage: Unavailable"'); + $original->update(['team_id' => $this->teamB->id]); + $html = Livewire::test(BackupExecutions::class, ['service' => $this->ownService])->html(); + expect(substr_count($html, 'data-tooltip="S3 storage: Unavailable"'))->toBe(2) + ->and($html)->not->toContain('Cloudflare R2'); });