mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-23 18:25:03 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<form class="application-settings-form flex w-full flex-col gap-4" wire:submit="submit">
|
||||
@if ($service)
|
||||
<x-forms.listbox id="selectedDatabaseUuid" label="Database" :options="$databaseOptions"
|
||||
empty-text="No databases in this service support scheduled backups." required />
|
||||
@error('selectedDatabaseUuid')
|
||||
<p class="text-xs text-error">{{ $message }}</p>
|
||||
@enderror
|
||||
@endif
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency"
|
||||
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency"
|
||||
required />
|
||||
|
||||
@@ -5,17 +5,23 @@
|
||||
filterOpen: false,
|
||||
sortOpen: false,
|
||||
backups: @js($backups->map(fn ($backup) => [
|
||||
'id' => (string) $backup->id,
|
||||
'id' => 'storage:'.$backup->id,
|
||||
'name' => strtolower($backup->targetName()),
|
||||
'type' => strtolower($backup->targetType()),
|
||||
'frequency' => strtolower($backup->frequency),
|
||||
'createdAt' => $backup->created_at?->timestamp ?? 0,
|
||||
])->values()),
|
||||
])->concat($databaseBackups->map(fn ($backup) => [
|
||||
'id' => 'database:'.$backup->id,
|
||||
'name' => strtolower($backup->database->human_name ?: $backup->database->name),
|
||||
'type' => 'database',
|
||||
'frequency' => strtolower($backup->frequency),
|
||||
'createdAt' => $backup->created_at?->timestamp ?? 0,
|
||||
]))->values()),
|
||||
filterOptions: @js(collect([['value' => 'all', 'label' => 'All targets']])->merge(
|
||||
$backups->map(fn ($backup) => [
|
||||
'value' => strtolower($backup->targetType()),
|
||||
'label' => $backup->targetType(),
|
||||
])->unique('value')->values()
|
||||
])->push(['value' => 'database', 'label' => 'Database'])->unique('value')->values()
|
||||
)->values()),
|
||||
sortOptions: [
|
||||
{ value: 'target_asc', label: 'Target A–Z' },
|
||||
@@ -57,21 +63,48 @@
|
||||
current-route="project.service.volume-backups.index" />
|
||||
|
||||
<div class="application-settings-form min-w-0 flex flex-col gap-6">
|
||||
<x-application.settings-section title="Storage backups"
|
||||
helper="Schedule backups for persistent volumes and directory mounts attached to this service.">
|
||||
<x-application.settings-section title="Backups"
|
||||
helper="Manage database, persistent volume, and directory backup schedules for this service.">
|
||||
@can('update', $service)
|
||||
<x-slot:actions>
|
||||
<x-modal-input title="New scheduled backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<button type="button"
|
||||
class="button button-highlighted">
|
||||
<div x-data="{ dropdownOpen: false }">
|
||||
<div class="relative" @click.outside="dropdownOpen = false">
|
||||
<x-forms.button class="button-highlighted" @click="dropdownOpen = !dropdownOpen"
|
||||
aria-haspopup="menu" x-bind:aria-expanded="dropdownOpen">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
Add
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:project.service.volume-backup.create :service="$service"
|
||||
wire:key="create-volume-backup-{{ $service->id }}" />
|
||||
</x-modal-input>
|
||||
Add backup
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</x-forms.button>
|
||||
|
||||
<div x-show="dropdownOpen" x-cloak role="menu" x-transition.origin.top.left
|
||||
class="listbox-panel left-0! right-auto! z-[90]! w-52! min-w-52! sm:left-auto! sm:right-0!">
|
||||
<x-modal-input title="New storage backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<button type="button" role="menuitem" @click="dropdownOpen = false"
|
||||
class="listbox-option justify-start! gap-2.5!">
|
||||
<x-reicon name="storages" class="size-3.5" />
|
||||
Storage backup
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:project.service.volume-backup.create :service="$service"
|
||||
wire:key="create-volume-backup-{{ $service->id }}" />
|
||||
</x-modal-input>
|
||||
@if ($databaseTargets->isNotEmpty())
|
||||
<x-modal-input title="New database backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<button type="button" role="menuitem" @click="dropdownOpen = false"
|
||||
class="listbox-option justify-start! gap-2.5!">
|
||||
<x-reicon name="database" class="size-3.5" />
|
||||
Database backup
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:project.database.create-scheduled-backup :service="$service"
|
||||
wire:key="create-service-database-backup-{{ $service->id }}" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
@endcan
|
||||
|
||||
@@ -79,19 +112,19 @@
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Schedules</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->count() }}
|
||||
{{ $backups->count() + $databaseBackups->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Enabled</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->where('enabled', true)->count() }}
|
||||
{{ $backups->where('enabled', true)->count() + $databaseBackups->where('enabled', true)->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Total executions</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->sum('executions_count') }}
|
||||
{{ $backups->sum('executions_count') + $databaseBackups->sum('executions_count') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,25 +198,66 @@
|
||||
|
||||
<div @class([
|
||||
'application-settings-section-body w-full',
|
||||
'is-flush' => $backups->isNotEmpty(),
|
||||
'is-flush' => $backups->isNotEmpty() || $databaseBackups->isNotEmpty(),
|
||||
])>
|
||||
<div x-cloak x-show="backups.length > 0 && filteredBackups.length === 0">
|
||||
<x-empty size="sm" title="No backups found"
|
||||
description="No scheduled backups match your search." />
|
||||
</div>
|
||||
|
||||
@if ($backups->isNotEmpty())
|
||||
@if ($backups->isNotEmpty() || $databaseBackups->isNotEmpty())
|
||||
<div class="data-table w-full overflow-x-auto" x-show="filteredBackups.length > 0">
|
||||
<div class="data-table-header backup-table-grid">
|
||||
<div class="data-table-header backup-table-grid service-backup-table-grid">
|
||||
<span>Target</span>
|
||||
<span>Type</span>
|
||||
<span>Schedule</span>
|
||||
<span>Status</span>
|
||||
<span>S3</span>
|
||||
<span>Last run</span>
|
||||
<span class="text-right">Executions</span>
|
||||
</div>
|
||||
|
||||
@foreach ($databaseBackups as $databaseBackup)
|
||||
@php
|
||||
$latestExecution = $databaseBackup->latest_log;
|
||||
$status = $latestExecution?->status;
|
||||
$statusLabel = match ($status) {
|
||||
'running' => 'In progress',
|
||||
'success' => 'Success',
|
||||
'failed' => 'Failed',
|
||||
default => $databaseBackup->enabled ? 'Waiting' : 'Disabled',
|
||||
};
|
||||
$statusType = match ($status) {
|
||||
'running' => 'warning',
|
||||
'success' => 'success',
|
||||
'failed' => 'error',
|
||||
default => 'neutral',
|
||||
};
|
||||
$databaseBackupId = 'database:'.$databaseBackup->id;
|
||||
@endphp
|
||||
<a wire:key="database-backup-{{ $databaseBackup->uuid }}"
|
||||
x-show="isVisible(@js($databaseBackupId))"
|
||||
x-bind:style="{ order: backupOrder(@js($databaseBackupId)) }"
|
||||
href="{{ route('project.service.database.backup.show', [
|
||||
...$parameters,
|
||||
'stack_service_uuid' => $databaseBackup->database->uuid,
|
||||
'backup_uuid' => $databaseBackup->uuid,
|
||||
]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="data-table-row backup-table-grid text-[13px] text-neutral-700 service-backup-table-grid dark:text-fg-dim">
|
||||
<span class="min-w-0 truncate font-medium text-neutral-950 dark:text-fg">
|
||||
{{ $databaseBackup->database->human_name ?: $databaseBackup->database->name }}
|
||||
</span>
|
||||
<span>Database</span>
|
||||
<span>{{ $databaseBackup->frequency }}</span>
|
||||
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
||||
<span>
|
||||
<x-status-badge :status="$databaseBackup->save_s3 ? ($databaseBackup->s3 ? 'Configured' : 'Unavailable') : 'Not set'"
|
||||
:type="$databaseBackup->save_s3 ? ($databaseBackup->s3 ? 'success' : 'error') : 'neutral'" />
|
||||
</span>
|
||||
<span>{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
@foreach ($backups as $backup)
|
||||
@php
|
||||
$latestExecution = $backup->latestExecution;
|
||||
@@ -202,11 +276,11 @@
|
||||
};
|
||||
@endphp
|
||||
<a wire:key="volume-backup-{{ $backup->uuid }}"
|
||||
x-show="isVisible(@js((string) $backup->id))"
|
||||
x-bind:style="{ order: backupOrder(@js((string) $backup->id)) }"
|
||||
x-show="isVisible(@js('storage:'.$backup->id))"
|
||||
x-bind:style="{ order: backupOrder(@js('storage:'.$backup->id)) }"
|
||||
href="{{ route('project.service.volume-backups.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="data-table-row backup-table-grid text-[13px] text-neutral-700 dark:text-fg-dim">
|
||||
class="data-table-row backup-table-grid text-[13px] text-neutral-700 service-backup-table-grid dark:text-fg-dim">
|
||||
<span class="min-w-0 truncate font-medium text-neutral-950 dark:text-fg"
|
||||
title="{{ $backup->targetName() }}">
|
||||
{{ $backup->targetName() }}
|
||||
@@ -221,15 +295,12 @@
|
||||
<span>
|
||||
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backup->executions_count }}
|
||||
</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="No scheduled backups"
|
||||
description="Add a persistent volume or directory backup schedule to protect service data."
|
||||
description="Add a database, persistent volume, or directory backup schedule to protect service data."
|
||||
icon-name="storages" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -94,6 +94,36 @@ it('creates a service database backup without S3 and opens its configuration', f
|
||||
->and($backup->s3_storage_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('selects a service database when creating a backup from the unified backups page', function () {
|
||||
$service = Service::factory()->create([
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
ServiceDatabase::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'primary',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'custom_type' => 'postgresql',
|
||||
]);
|
||||
$analytics = ServiceDatabase::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'analytics',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'custom_type' => 'postgresql',
|
||||
]);
|
||||
|
||||
Livewire::test(CreateScheduledBackup::class, ['service' => $service])
|
||||
->assertSee('Database')
|
||||
->assertSee('analytics')
|
||||
->set('selectedDatabaseUuid', $analytics->uuid)
|
||||
->set('frequency', 'daily')
|
||||
->call('submit');
|
||||
|
||||
expect(ScheduledDatabaseBackup::query()->sole()->database->is($analytics))->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates a clickhouse backup for its configured database', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
|
||||
@@ -80,13 +80,35 @@ it('shows the service sidebar on the storage backups page', function () {
|
||||
->toContain('xl:grid-cols-[210px_minmax(0,1fr)]');
|
||||
});
|
||||
|
||||
it('links compose database backups to the parent service backups page', function () {
|
||||
it('combines service database and storage backups in one section', function () {
|
||||
$backups = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
|
||||
$styles = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect(substr_count($backups, '<x-application.settings-section '))->toBe(1)
|
||||
->and(substr_count($backups, 'data-table-header backup-table-grid'))->toBe(1)
|
||||
->and($backups)->toContain('Storage backup')->toContain('Database backup')
|
||||
->not->toContain('data-table-header scheduled-backups-table-grid')
|
||||
->not->toContain('>Database backups</h3>')
|
||||
->not->toContain('>Storage backups</h3>')
|
||||
->toContain("'application-settings-section-body w-full'")
|
||||
->toContain('class="data-table w-full overflow-x-auto"')
|
||||
->toContain('backup-table-grid service-backup-table-grid')
|
||||
->not->toContain('<span class="text-right">Executions</span>')
|
||||
->toContain('class="data-table-row backup-table-grid text-[13px]')
|
||||
->toContain('class="listbox-option justify-start! gap-2.5!"')
|
||||
->toContain('x-data="{ dropdownOpen: false }"')
|
||||
->toContain('class="listbox-panel left-0! right-auto! z-[90]! w-52! min-w-52! sm:left-auto! sm:right-0!"')
|
||||
->not->toContain('<x-dropdown');
|
||||
|
||||
expect($styles)->toContain('.service-backup-table-grid');
|
||||
});
|
||||
|
||||
it('links compose database backups to the unified service backups page', function () {
|
||||
$sidebar = file_get_contents(resource_path('views/components/service-database/sidebar.blade.php'));
|
||||
|
||||
expect($sidebar)
|
||||
->toContain("'route' => 'project.service.volume-backups.index'")
|
||||
->toContain("'parameters' => \$serviceParameters")
|
||||
->toContain("\$item['parameters'] ?? \$parameters")
|
||||
->not->toContain("'route' => 'project.service.database.backups'");
|
||||
});
|
||||
|
||||
|
||||
@@ -202,3 +202,38 @@ test('service database backup schedules use dedicated general retention and exec
|
||||
->assertDontSee('Number of backups to keep')
|
||||
->assertDontSee('Cleanup Failed Backups');
|
||||
});
|
||||
|
||||
test('service storage backups page includes schedules from all compose databases', function () {
|
||||
$secondDatabase = ServiceDatabase::create([
|
||||
'service_id' => $this->ownService->id,
|
||||
'name' => 'analytics-db',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'custom_type' => 'postgresql',
|
||||
]);
|
||||
|
||||
foreach ([$this->ownServiceDatabase, $secondDatabase] as $database) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'team_id' => $this->teamA->id,
|
||||
'description' => $database->name.' backup',
|
||||
'frequency' => 'daily',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => $database->getMorphClass(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->get(route('project.service.volume-backups.index', [
|
||||
'project_uuid' => $this->projectA->uuid,
|
||||
'environment_uuid' => $this->environmentA->uuid,
|
||||
'service_uuid' => $this->ownService->uuid,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('>Database</span>', false)
|
||||
->assertSee('own-db')
|
||||
->assertSee('analytics-db')
|
||||
->assertSee(route('project.service.database.backups', [
|
||||
'project_uuid' => $this->projectA->uuid,
|
||||
'environment_uuid' => $this->environmentA->uuid,
|
||||
'service_uuid' => $this->ownService->uuid,
|
||||
'stack_service_uuid' => $this->ownServiceDatabase->uuid,
|
||||
]), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user