Store s3_storage_id on scheduled volume backup executions so retention and recovery use the S3 that received the upload. Extract DeleteScheduledVolumeBackup for UI and resource deletion, and isolate database backup retention failures so cleanup errors do not fail the job.
232 lines
15 KiB
PHP
232 lines
15 KiB
PHP
<div x-data="{
|
|
search: '',
|
|
backups: @js($database->scheduledBackups->map(fn ($backup) => [
|
|
'name' => strtolower($database->name),
|
|
'frequency' => strtolower($backup->frequency),
|
|
's3_storage' => strtolower($backup->s3?->name ?? ''),
|
|
])->values()),
|
|
hasMatches() {
|
|
const query = this.search.toLowerCase();
|
|
|
|
return this.backups.some((backup) => backup.name.includes(query) || backup.frequency.includes(query) || backup.s3_storage.includes(query));
|
|
},
|
|
}">
|
|
<div class="flex flex-col gap-2">
|
|
@if ($database->is_migrated && blank($database->custom_type))
|
|
<div>
|
|
<div>Select the type of
|
|
database to enable automated backups.</div>
|
|
<div class="pb-4"> If your database is not listed, automated backups are not supported.</div>
|
|
<form wire:submit="setCustomType" class="flex gap-2 items-end">
|
|
<div class="w-96">
|
|
<x-forms.select label="Type" id="custom_type">
|
|
<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>
|
|
<x-forms.button type="submit">Set</x-forms.button>
|
|
</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, frequency, or S3 storage..." />
|
|
</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 x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase()) || @js(strtolower($backup->s3?->name ?? '')).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 &&
|
|
data_get($backup->latest_log, 'status') === 'running',
|
|
'border-error' =>
|
|
$backup->latest_log &&
|
|
data_get($backup->latest_log, 'status') === 'failed',
|
|
'border-success' =>
|
|
$backup->latest_log &&
|
|
data_get($backup->latest_log, 'status') === 'success',
|
|
'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
|
|
]) {{ wireNavigate() }}
|
|
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
|
|
@if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
|
|
<div class="absolute top-2 right-2">
|
|
<x-loading />
|
|
</div>
|
|
@endif
|
|
<div class="flex items-center gap-2 mb-2">
|
|
@if ($backup->latest_log)
|
|
<span @class([
|
|
'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
|
|
'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300 dark:shadow-blue-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'running',
|
|
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'failed',
|
|
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'success',
|
|
])>
|
|
@php
|
|
$statusText = match (data_get($backup->latest_log, 'status')) {
|
|
'success' => 'Success',
|
|
'running' => 'In Progress',
|
|
'failed' => 'Failed',
|
|
default => ucfirst(data_get($backup->latest_log, 'status')),
|
|
};
|
|
@endphp
|
|
{{ $statusText }}
|
|
</span>
|
|
@else
|
|
<span
|
|
class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-neutral-800 dark:text-gray-200">
|
|
No executions yet
|
|
</span>
|
|
@endif
|
|
<h3 class="font-semibold">{{ $backup->frequency }}</h3>
|
|
</div>
|
|
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
|
@if ($backup->latest_log)
|
|
@if (data_get($backup->latest_log, 'status') === 'running')
|
|
<span
|
|
title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}">
|
|
Running for
|
|
{{ calculateDuration(data_get($backup->latest_log, 'created_at'), now()) }}
|
|
</span>
|
|
@else
|
|
<span
|
|
title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} Ended: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}">
|
|
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
|
({{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }})
|
|
•
|
|
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->format('M j, H:i') }}
|
|
</span>
|
|
@endif
|
|
@if (data_get($backup->latest_log, 'status') === 'success')
|
|
@php
|
|
$size = data_get($backup->latest_log, 'size', 0);
|
|
@endphp
|
|
@if ($size > 0)
|
|
• Size: {{ formatBytes($size) }}
|
|
@endif
|
|
@endif
|
|
@if ($backup->save_s3)
|
|
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
|
|
@endif
|
|
@else
|
|
Last Run: Never • Total Executions: 0
|
|
@if ($backup->save_s3)
|
|
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</a>
|
|
@else
|
|
<a x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase()) || @js(strtolower($backup->s3?->name ?? '')).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 &&
|
|
data_get($backup->latest_log, 'status') === 'running',
|
|
'border-error' =>
|
|
$backup->latest_log &&
|
|
data_get($backup->latest_log, 'status') === 'failed',
|
|
'border-success' =>
|
|
$backup->latest_log &&
|
|
data_get($backup->latest_log, 'status') === 'success',
|
|
'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
|
|
]) {{ wireNavigate() }}
|
|
href="{{ route('project.service.database.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
|
|
@if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
|
|
<div class="absolute top-2 right-2">
|
|
<x-loading />
|
|
</div>
|
|
@endif
|
|
<div class="flex items-center gap-2 mb-2">
|
|
@if ($backup->latest_log)
|
|
<span @class([
|
|
'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
|
|
'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300 dark:shadow-blue-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'running',
|
|
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'failed',
|
|
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' =>
|
|
data_get($backup->latest_log, 'status') === 'success',
|
|
])>
|
|
@php
|
|
$statusText = match (data_get($backup->latest_log, 'status')) {
|
|
'success' => 'Success',
|
|
'running' => 'In Progress',
|
|
'failed' => 'Failed',
|
|
default => ucfirst(data_get($backup->latest_log, 'status')),
|
|
};
|
|
@endphp
|
|
{{ $statusText }}
|
|
</span>
|
|
@else
|
|
<span
|
|
class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-neutral-800 dark:text-gray-200">
|
|
No executions yet
|
|
</span>
|
|
@endif
|
|
<h3 class="font-semibold">{{ $backup->frequency }} Backup</h3>
|
|
</div>
|
|
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
|
@if ($backup->latest_log)
|
|
@if (data_get($backup->latest_log, 'status') === 'running')
|
|
<span
|
|
title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}">
|
|
Running for
|
|
{{ calculateDuration(data_get($backup->latest_log, 'created_at'), now()) }}
|
|
</span>
|
|
@else
|
|
<span
|
|
title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} Ended: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}">
|
|
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
|
({{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }})
|
|
•
|
|
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->format('M j, H:i') }}
|
|
</span>
|
|
@endif
|
|
@if (data_get($backup->latest_log, 'status') === 'success')
|
|
@php
|
|
$size = data_get($backup->latest_log, 'size', 0);
|
|
@endphp
|
|
@if ($size > 0)
|
|
• Size: {{ formatBytes($size) }}
|
|
@endif
|
|
@endif
|
|
@if ($backup->save_s3)
|
|
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
|
|
@endif
|
|
<br>Total Executions: {{ $backup->executions()->count() }}
|
|
@php
|
|
$successCount = $backup->executions()->where('status', 'success')->count();
|
|
$totalCount = $backup->executions()->count();
|
|
$successRate = $totalCount > 0 ? round(($successCount / $totalCount) * 100) : 0;
|
|
@endphp
|
|
@if ($totalCount > 0)
|
|
• Success Rate: <span @class([
|
|
'font-medium',
|
|
'text-green-600' => $successRate >= 80,
|
|
'text-warning-600' => $successRate >= 50 && $successRate < 80,
|
|
'text-red-600' => $successRate < 50,
|
|
])>{{ $successRate }}%</span>
|
|
({{ $successCount }}/{{ $totalCount }})
|
|
@endif
|
|
@else
|
|
Last Run: Never • Total Executions: 0
|
|
@if ($backup->save_s3)
|
|
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</a>
|
|
@endif
|
|
@empty
|
|
<div>No scheduled backups configured.</div>
|
|
@endforelse
|
|
@endif
|
|
</div>
|
|
</div>
|