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.
This commit is contained in:
Andras Bacsai committed 2026-08-08 00:12:56 +02:00
1 parent 12b14665b5
commit c0b75d04a6
8 files changed
+288 -51

No files matched your search

@@ -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);
}
}
@@ -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