feat(backup): implement custom database type selection and enhance scheduled backups management

This commit is contained in:
Andras Bacsai
2025-04-30 16:44:44 +02:00
parent debfcb7028
commit fe24296de7
9 changed files with 74 additions and 36 deletions

View File

@@ -19,6 +19,8 @@ class ScheduledBackups extends Component
public $s3s; public $s3s;
public string $custom_type = 'mysql';
protected $listeners = ['refreshScheduledBackups']; protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId']; protected $queryString = ['selectedBackupId'];
@@ -49,6 +51,14 @@ class ScheduledBackups extends Component
} }
} }
public function setCustomType()
{
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
}
public function delete($scheduled_backup_id): void public function delete($scheduled_backup_id): void
{ {
$this->database->scheduledBackups->find($scheduled_backup_id)->delete(); $this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@@ -62,5 +72,6 @@ class ScheduledBackups extends Component
if ($id) { if ($id) {
$this->setSelectedBackup($id); $this->setSelectedBackup($id);
} }
$this->dispatch('refreshScheduledBackups');
} }
} }

View File

@@ -98,6 +98,7 @@ class Database extends Component
'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled, 'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled,
'image' => $serviceDatabase->image, 'image' => $serviceDatabase->image,
'service_id' => $service->id, 'service_id' => $service->id,
'is_migrated' => true,
]); ]);
$serviceDatabase->delete(); $serviceDatabase->delete();
DB::commit(); DB::commit();

View File

@@ -24,7 +24,7 @@ class Index extends Component
public $s3s; public $s3s;
protected $listeners = ['generateDockerCompose']; protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh'];
public function mount() public function mount()
{ {

View File

@@ -88,6 +88,7 @@ class ServiceApplicationView extends Component
'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled, 'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled,
'image' => $serviceApplication->image, 'image' => $serviceApplication->image,
'service_id' => $service->id, 'service_id' => $service->id,
'is_migrated' => true,
]); ]);
$serviceApplication->delete(); $serviceApplication->delete();
DB::commit(); DB::commit();

View File

@@ -16,6 +16,7 @@ class ServiceDatabase extends BaseModel
static::deleting(function ($service) { static::deleting(function ($service) {
$service->persistentStorages()->delete(); $service->persistentStorages()->delete();
$service->fileStorages()->delete(); $service->fileStorages()->delete();
$service->scheduledBackups()->delete();
}); });
static::saving(function ($service) { static::saving(function ($service) {
if ($service->isDirty('status')) { if ($service->isDirty('status')) {
@@ -77,6 +78,9 @@ class ServiceDatabase extends BaseModel
public function databaseType() public function databaseType()
{ {
if (filled($this->custom_type)) {
return 'standalone-'.$this->custom_type;
}
$image = str($this->image)->before(':'); $image = str($this->image)->before(':');
if ($image->contains('supabase/postgres')) { if ($image->contains('supabase/postgres')) {
$finalImage = 'supabase/postgres'; $finalImage = 'supabase/postgres';
@@ -141,6 +145,7 @@ class ServiceDatabase extends BaseModel
str($this->databaseType())->contains('postgres') || str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') || str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') || str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongo'); str($this->databaseType())->contains('mongo') ||
filled($this->custom_type);
} }
} }

View File

@@ -1,33 +1,52 @@
<div> <div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse($database->scheduledBackups as $backup) @if ($database->is_migrated && blank($database->custom_type))
@if ($type == 'database') <div>
<a class="box" <div>Select the type of
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> database to enable automated backups.</div>
<div class="flex flex-col"> <div class="pb-4"> If your database is not listed, automated backups are not supported.</div>
<div>Frequency: {{ $backup->frequency }} <form wire:submit="setCustomType" class="flex gap-2 items-end">
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) <div class="w-96">
</div> <x-forms.select label="Type" id="custom_type">
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> <option selected value="mysql">MySQL</option>
<option value="mariadb">MariaDB</option>
<option value="postgresql">PostgreSQL</option>
<option value="mongodb">MongoDB</option>
</x-forms.select>
</div> </div>
</a> <x-forms.button type="submit">Set</x-forms.button>
@else </form>
<div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> </div>
<div @class([ @else
'border-coollabs' => @forelse($database->scheduledBackups as $backup)
data_get($backup, 'id') === data_get($selectedBackup, 'id'), @if ($type == 'database')
'flex flex-col border-l-2 border-transparent', <a class="box"
])> href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div>Frequency: {{ $backup->frequency }} <div class="flex flex-col">
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) <div>Frequency: {{ $backup->frequency }}
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div>
</a>
@else
<div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
<div @class([
'border-coollabs' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
'flex flex-col border-l-2 border-transparent',
])>
<div>Frequency: {{ $backup->frequency }}
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div> </div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div> </div>
</div> @endif
@endif @empty
@empty <div>No scheduled backups configured.</div>
<div>No scheduled backups configured.</div> @endforelse
@endforelse @endif
</div> </div>
@if ($type === 'service-database' && $selectedBackup) @if ($type === 'service-database' && $selectedBackup)
<div class="pt-10"> <div class="pt-10">

View File

@@ -138,7 +138,7 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@if ($database->isBackupSolutionAvailable()) @if ($database->isBackupSolutionAvailable() || $database->is_migrated)
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups"> href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
Backups Backups

View File

@@ -10,7 +10,7 @@
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
@if ($serviceDatabase?->isBackupSolutionAvailable()) @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item" <a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a> @click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a>
@endif @endif
@@ -33,18 +33,20 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
@if ($serviceDatabase->isBackupSolutionAvailable()) @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
<div x-cloak x-show="activeTab === 'backups'"> <div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 "> <div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2> <h2 class="pb-4">Scheduled Backups</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup"> @if (filled($serviceDatabase->custom_type))
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" /> <x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
</x-modal-input> <livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endif
</div> </div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" /> <livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div>
@endif @endif
</div> </div>
@endisset @endisset
</div> </div>
</div> </div>
</div>

View File

@@ -13,8 +13,7 @@
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below" confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" /> shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton <x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
submitAction="delete" {{-- :checkboxes="$checkboxes" --}} :actions="['The selected service application container will be stopped and permanently deleted.']" submitAction="delete" :actions="['The selected service application container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($application->name) }}"
confirmationText="{{ Str::headline($application->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below" confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" /> shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
</div> </div>