fix(backups): require valid S3 storage selection

Preserve S3 backups when a single valid storage is available, require
explicit selection when multiple storages exist, and disable S3 when none
are available.

Make backup action controls responsive on narrow screens.
This commit is contained in:
Andras Bacsai committed 2026-07-01 11:14:20 +02:00
1 parent b93a91cc00
commit 22b31f5671
8 files changed
+157 -50

No files matched your search

@@ -129,7 +129,11 @@
}
}"
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@class([
'relative h-auto',
'w-full' => $buttonFullWidth,
'w-auto' => ! $buttonFullWidth,
])>
@if (isset($trigger))
<div @click="modalOpen=true">
{{ $trigger }}
@@ -1,32 +1,37 @@
<form wire:submit="submit">
<div class="flex gap-2 pb-2">
<div class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-start sm:justify-between">
<h2>Scheduled Backup</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if (str($status)->startsWith('running'))
<x-forms.button wire:click='backupNow'>Backup Now</x-forms.button>
@endif
@if ($backup->database_id !== 0)
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"
isErrorButton submitAction="delete" :checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name" />
@endif
<div class="grid grid-cols-1 gap-2 min-[420px]:grid-cols-2 sm:flex sm:flex-wrap sm:justify-end">
<x-forms.button type="submit" class="w-full sm:w-auto">
Save
</x-forms.button>
@if (str($status)->startsWith('running'))
<x-forms.button wire:click='backupNow' class="w-full sm:w-auto">Backup Now</x-forms.button>
@endif
@if ($backup->database_id !== 0)
<div class="min-[420px]:col-span-2 sm:col-span-1">
<x-modal-confirmation title="Confirm Backup Schedule Deletion?"
buttonTitle="Delete Backups and Schedule" buttonFullWidth isErrorButton submitAction="delete"
:checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name" />
</div>
@endif
</div>
</div>
<div class="w-64 pb-2">
<div class="w-full max-w-md pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($s3s->count() > 0)
@if ($availableS3Storages->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@else
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@endif
@if ($backup->save_s3)
@if ($saveS3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
@@ -34,11 +39,11 @@
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div>
@if ($backup->save_s3)
<div class="pb-6">
@if ($saveS3)
<div class="w-full max-w-md pb-6">
<x-forms.select id="s3StorageId" label="S3 Storage" required>
<option value="default" disabled>Select a S3 storage</option>
@foreach ($s3s as $s3)
@foreach ($availableS3Storages as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
@@ -80,7 +85,7 @@
@endif
@endif
</div>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Frequency" id="frequency" required />
<x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
@@ -98,7 +103,7 @@
<div class="flex gap-6 flex-col">
<div>
<h4 class="mb-3 font-medium">Local Backup Retention</h4>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
@@ -111,10 +116,10 @@
</div>
</div>
@if ($backup->save_s3)
@if ($saveS3)
<div>
<h4 class="mb-3 font-medium">S3 Storage Retention</h4>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
@@ -1,7 +1,7 @@
<div wire:init='refreshBackupExecutions'>
@isset($backup)
<div class="flex items-center gap-2">
<h3 class="py-4">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
<div class="flex flex-col gap-3 py-4 sm:flex-row sm:items-center sm:justify-between">
<h3 class="py-0">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
@if ($executions_count > 0)
<div class="flex items-center gap-2">
<x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
@@ -21,13 +21,15 @@
</x-forms.button>
</div>
@endif
<x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button>
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton
submitAction="cleanupDeleted()"
:actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']"
confirmationText="cleanup deleted backups"
confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
shortConfirmationLabel="Confirmation" />
<div class="grid grid-cols-1 gap-2 min-[420px]:grid-cols-2 sm:flex sm:flex-wrap sm:justify-end">
<x-forms.button wire:click='cleanupFailed' class="w-full sm:w-auto">Cleanup Failed Backups</x-forms.button>
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton
buttonFullWidth submitAction="cleanupDeleted()"
:actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']"
confirmationText="cleanup deleted backups"
confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
shortConfirmationLabel="Confirmation" />
</div>
</div>
<div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
class="flex flex-col gap-4">
@@ -87,7 +89,7 @@
<div class="text-gray-600 dark:text-gray-400 text-sm">
Location: {{ data_get($execution, 'filename', 'N/A') }}
</div>
<div class="flex items-center gap-3 mt-2">
<div class="flex flex-col gap-2 mt-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
<div class="text-gray-600 dark:text-gray-400 text-sm">
Backup Availability:
</div>
@@ -154,9 +156,9 @@
<pre class="whitespace-pre-wrap text-sm">{{ data_get($execution, 'message') }}</pre>
</div>
@endif
<div class="flex gap-2 mt-4">
<div class="flex flex-col gap-2 mt-4 min-[420px]:flex-row min-[420px]:flex-wrap">
@if (data_get($execution, 'status') === 'success')
<x-forms.button class="dark:hover:bg-coolgray-400"
<x-forms.button class="w-full dark:hover:bg-coolgray-400 min-[420px]:w-auto"
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif
@php
@@ -6,7 +6,7 @@
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" />
<div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions :backup="$backup" />
</div>
</div>
@@ -216,7 +216,7 @@
@if ($type === 'service-database' && $selectedBackup)
<div class="pt-10">
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:s3s="$s3s" :status="data_get($database, 'status')" />
:available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
:database="$database" />
</div>
@@ -27,7 +27,7 @@
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
</div>
</div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<div class="py-4">
<livewire:project.database.backup-executions :backup="$backup" />
</div>