feat(backups): add scheduled persistent volume backups

This commit is contained in:
Andras Bacsai committed 2026-07-15 15:47:57 +02:00
1 parent 05dc2c65f6
commit 63961e0799
36 files changed
+3506 -31

No files matched your search

@@ -5,6 +5,13 @@
<x-forms.button type="submit" class="w-full sm:w-auto">
Save
</x-forms.button>
@if (!$backupEnabled)
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled" isHighlighted>Enable Backup</x-forms.button>
@else
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled">Disable Backup</x-forms.button>
@endif
@if (str($status)->startsWith('running'))
<x-forms.button wire:click='backupNow' class="w-full sm:w-auto">Backup Now</x-forms.button>
@endif
@@ -27,9 +34,11 @@
</div>
</div>
<div class="w-full max-w-md pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($availableS3Storages->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@elseif ($saveS3)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3"
helper="The configured S3 storage is no longer available. Disable S3 backups or configure a usable S3 storage." />
@else
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@@ -2,8 +2,6 @@
@php
$databasePageItems = [
['label' => 'Configuration', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
[
'label' => 'Backups',
'route' => 'project.database.backup.index',
@@ -15,6 +13,8 @@
'App\Models\StandaloneMariadb',
]),
],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
];
$databaseConfigurationItems = [
@@ -190,16 +190,6 @@
Configuration
</a>
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
Logs
</a>
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
Terminal
</a>
@endcan
@if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
@@ -210,6 +200,16 @@
Backups
</a>
@endif
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
Logs
</a>
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
Terminal
</a>
@endcan
</nav>
@if ($database->destination->server->isFunctional())
@@ -1,4 +1,15 @@
<div>
<div x-data="{
search: '',
backups: @js($database->scheduledBackups->map(fn ($backup) => [
'name' => strtolower($database->name),
'frequency' => strtolower($backup->frequency),
])->values()),
hasMatches() {
const query = this.search.toLowerCase();
return this.backups.some((backup) => backup.name.includes(query) || backup.frequency.includes(query));
},
}">
<div class="flex flex-col gap-2">
@if ($database->is_migrated && blank($database->custom_type))
<div>
@@ -18,9 +29,16 @@
</form>
</div>
@else
<div class="max-w-md pb-4">
<x-forms.input id="null" type="search" x-model="search"
placeholder="Search by database name or frequency..." />
</div>
<div x-cloak x-show="search !== '' && backups.length > 0 && !hasMatches()">
No scheduled backups match your search.
</div>
@forelse($database->scheduledBackups as $backup)
@if ($type == 'database')
<a @class([
<a x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase())" @class([
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
'border-blue-500/50 border-dashed' =>
$backup->latest_log &&
@@ -105,7 +123,7 @@
</div>
</a>
@else
<div @class([
<div x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase())" @class([
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
'bg-gray-200 dark:bg-coolgray-200' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),