feat(storage): add storage management for backup schedules
Add ability to move backups between S3 storages and disable S3 backups. Refactor storage resources view from cards to table layout with search functionality and storage selection dropdowns.
This commit is contained in:
parent
86c8ec9c20
commit
ce5e736b00
3 changed files with 165 additions and 79 deletions
|
|
@ -10,6 +10,61 @@ class Resources extends Component
|
|||
{
|
||||
public S3Storage $storage;
|
||||
|
||||
public array $selectedStorages = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)
|
||||
->where('save_s3', true)
|
||||
->get();
|
||||
|
||||
foreach ($backups as $backup) {
|
||||
$this->selectedStorages[$backup->id] = $this->storage->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function disableS3(int $backupId): void
|
||||
{
|
||||
$backup = ScheduledDatabaseBackup::findOrFail($backupId);
|
||||
|
||||
$backup->update([
|
||||
'save_s3' => false,
|
||||
's3_storage_id' => null,
|
||||
]);
|
||||
|
||||
unset($this->selectedStorages[$backupId]);
|
||||
|
||||
$this->dispatch('success', 'S3 disabled.', 'S3 backup has been disabled for this schedule.');
|
||||
}
|
||||
|
||||
public function moveBackup(int $backupId): void
|
||||
{
|
||||
$backup = ScheduledDatabaseBackup::findOrFail($backupId);
|
||||
$newStorageId = $this->selectedStorages[$backupId] ?? null;
|
||||
|
||||
if (! $newStorageId || (int) $newStorageId === $this->storage->id) {
|
||||
$this->dispatch('error', 'No change.', 'The backup is already using this storage.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$newStorage = S3Storage::where('id', $newStorageId)
|
||||
->where('team_id', $this->storage->team_id)
|
||||
->first();
|
||||
|
||||
if (! $newStorage) {
|
||||
$this->dispatch('error', 'Storage not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$backup->update(['s3_storage_id' => $newStorage->id]);
|
||||
|
||||
unset($this->selectedStorages[$backupId]);
|
||||
|
||||
$this->dispatch('success', 'Backup moved.', "Moved to {$newStorage->name}.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)
|
||||
|
|
@ -18,8 +73,13 @@ public function render()
|
|||
->get()
|
||||
->groupBy(fn ($backup) => $backup->database_type.'-'.$backup->database_id);
|
||||
|
||||
$allStorages = S3Storage::where('team_id', $this->storage->team_id)
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'is_usable']);
|
||||
|
||||
return view('livewire.storage.resources', [
|
||||
'groupedBackups' => $backups,
|
||||
'allStorages' => $allStorages,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ tbody {
|
|||
}
|
||||
|
||||
tr {
|
||||
@apply text-black dark:text-neutral-400 dark:hover:bg-black hover:bg-neutral-200;
|
||||
@apply text-black dark:text-neutral-400 dark:hover:bg-coolgray-300 hover:bg-neutral-100;
|
||||
}
|
||||
|
||||
tr th {
|
||||
|
|
|
|||
|
|
@ -1,81 +1,107 @@
|
|||
<div>
|
||||
@forelse ($groupedBackups as $backups)
|
||||
@php
|
||||
$firstBackup = $backups->first();
|
||||
$database = $firstBackup->database;
|
||||
$databaseName = $database?->name ?? 'Deleted database';
|
||||
$resourceLink = null;
|
||||
$backupParams = null;
|
||||
if ($database && $database instanceof \App\Models\ServiceDatabase) {
|
||||
$service = $database->service;
|
||||
if ($service) {
|
||||
$environment = $service->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment) {
|
||||
$resourceLink = route('project.service.configuration', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'service_uuid' => $service->uuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} elseif ($database) {
|
||||
$environment = $database->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment) {
|
||||
$resourceLink = route('project.database.backup.index', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
]);
|
||||
$backupParams = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
];
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<div class="pb-6">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
@if ($resourceLink)
|
||||
<a {{ wireNavigate() }} href="{{ $resourceLink }}" class="text-lg font-bold dark:text-white hover:underline">{{ $databaseName }}</a>
|
||||
@else
|
||||
<span class="text-lg font-bold dark:text-white">{{ $databaseName }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
@foreach ($backups as $backup)
|
||||
@php
|
||||
$backupLink = null;
|
||||
if ($backupParams) {
|
||||
$backupLink = route('project.database.backup.execution', array_merge($backupParams, [
|
||||
'backup_uuid' => $backup->uuid,
|
||||
]));
|
||||
}
|
||||
@endphp
|
||||
@if ($backupLink)
|
||||
<a {{ wireNavigate() }} href="{{ $backupLink }}" @class(['gap-2 border cursor-pointer coolbox group'])>
|
||||
@else
|
||||
<div @class(['gap-2 border coolbox'])>
|
||||
@endif
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">{{ $backup->frequency }}</div>
|
||||
@if (!$backup->enabled)
|
||||
<span class="px-2 py-1 text-xs font-semibold text-yellow-800 bg-yellow-100 rounded dark:text-yellow-100 dark:bg-yellow-800 w-fit">
|
||||
Disabled
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if ($backupLink)
|
||||
</a>
|
||||
@else
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
<div x-data="{ search: '' }">
|
||||
<x-forms.input placeholder="Search resources..." x-model="search" id="null" />
|
||||
@if ($groupedBackups->count() > 0)
|
||||
<div class="overflow-x-auto pt-4">
|
||||
<div class="inline-block min-w-full">
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Database</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Frequency</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Status</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">S3 Storage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($groupedBackups as $backups)
|
||||
@php
|
||||
$firstBackup = $backups->first();
|
||||
$database = $firstBackup->database;
|
||||
$databaseName = $database?->name ?? 'Deleted database';
|
||||
$resourceLink = null;
|
||||
$backupParams = null;
|
||||
if ($database && $database instanceof \App\Models\ServiceDatabase) {
|
||||
$service = $database->service;
|
||||
if ($service) {
|
||||
$environment = $service->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment) {
|
||||
$resourceLink = route('project.service.configuration', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'service_uuid' => $service->uuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} elseif ($database) {
|
||||
$environment = $database->environment;
|
||||
$project = $environment?->project;
|
||||
if ($project && $environment) {
|
||||
$resourceLink = route('project.database.backup.index', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
]);
|
||||
$backupParams = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'database_uuid' => $database->uuid,
|
||||
];
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
@foreach ($backups as $backup)
|
||||
<tr class="dark:hover:bg-coolgray-300 hover:bg-neutral-100" x-show="search === '' || '{{ strtolower(addslashes($databaseName)) }}'.includes(search.toLowerCase()) || '{{ strtolower(addslashes($backup->frequency)) }}'.includes(search.toLowerCase())">
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
@if ($resourceLink)
|
||||
<a class="hover:underline" {{ wireNavigate() }} href="{{ $resourceLink }}">{{ $databaseName }} <x-internal-link /></a>
|
||||
@else
|
||||
{{ $databaseName }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
@php
|
||||
$backupLink = null;
|
||||
if ($backupParams) {
|
||||
$backupLink = route('project.database.backup.execution', array_merge($backupParams, [
|
||||
'backup_uuid' => $backup->uuid,
|
||||
]));
|
||||
}
|
||||
@endphp
|
||||
@if ($backupLink)
|
||||
<a class="hover:underline" {{ wireNavigate() }} href="{{ $backupLink }}">{{ $backup->frequency }} <x-internal-link /></a>
|
||||
@else
|
||||
{{ $backup->frequency }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm font-medium whitespace-nowrap">
|
||||
@if ($backup->enabled)
|
||||
<span class="text-green-500">Enabled</span>
|
||||
@else
|
||||
<span class="text-yellow-500">Disabled</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<select wire:model="selectedStorages.{{ $backup->id }}" class="w-full input">
|
||||
@foreach ($allStorages as $s3)
|
||||
<option value="{{ $s3->id }}" @disabled(!$s3->is_usable)>{{ $s3->name }}@if (!$s3->is_usable) (unusable)@endif</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<x-forms.button wire:click="moveBackup({{ $backup->id }})">Save</x-forms.button>
|
||||
<x-forms.button isError wire:click="disableS3({{ $backup->id }})" wire:confirm="Are you sure you want to disable S3 for this backup schedule?">Disable S3</x-forms.button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div>No backup schedules are using this storage.</div>
|
||||
@endforelse
|
||||
@else
|
||||
<div class="pt-4">No backup schedules are using this storage.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue