chore(storage): remove dead storages show component (#11615)

This commit is contained in:
🏔️ Peak
2026-09-03 18:22:18 +02:00
committed by GitHub
parent cc084235c1
commit 4ee460cfd0
7 changed files with 17 additions and 400 deletions
@@ -1,201 +0,0 @@
<?php
namespace App\Livewire\Project\Shared\Storages;
use App\Livewire\Project\Service\Storage as StorageComponent;
use App\Models\Application;
use App\Models\LocalPersistentVolume;
use App\Models\ScheduledVolumeBackup;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\On;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public LocalPersistentVolume $storage;
public $resource;
public bool $isReadOnly = false;
public bool $isFirst = true;
public bool $isService = false;
public ?string $startedAt = null;
public bool $supportsPreviewSuffix = false;
// Explicit properties
public string $name;
public string $mountPath;
public ?string $hostPath = null;
public bool $isPreviewSuffixEnabled = true;
public bool $hasEnabledBackup = false;
public ?string $backupUrl = null;
/**
* When true, parent already batched badge/url data skip per-row queries on mount.
*/
public bool $backupMetaHydrated = false;
/** When true, the Backup Configure Livewire modal is mounted (lazy). */
public bool $showBackupModal = false;
protected $validationAttributes = [
'name' => 'name',
'mountPath' => 'mount',
'hostPath' => 'host',
];
protected function rules(): array
{
return [
'name' => ValidationPatterns::volumeNameRules(),
'mountPath' => ['required', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
'hostPath' => ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
'isPreviewSuffixEnabled' => 'required|boolean',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::volumeNameMessages(),
[
'mountPath.regex' => 'Mount path must start with / and only contain safe path characters.',
'hostPath.regex' => 'Host path must start with / and only contain safe path characters.',
]
);
}
/**
* Sync data between component properties and model
*
* @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties.
*/
private function syncData(bool $toModel = false): void
{
if ($toModel) {
// Sync TO model (before save)
$this->storage->name = $this->name;
$this->storage->mount_path = $this->mountPath;
$this->storage->host_path = $this->hostPath;
$this->storage->is_preview_suffix_enabled = $this->isPreviewSuffixEnabled;
} else {
// Sync FROM model (on load/refresh)
$this->name = $this->storage->name;
$this->mountPath = $this->storage->mount_path;
$this->hostPath = $this->storage->host_path;
$this->isPreviewSuffixEnabled = $this->storage->is_preview_suffix_enabled ?? true;
}
}
public function mount(): void
{
$this->syncData(false);
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
// PR deployment volume suffixes only apply to git-based applications.
$this->supportsPreviewSuffix = $this->resource instanceof Application
&& $this->resource->git_based()
&& filled($this->resource->git_repository)
&& ! $this->isService;
// Parent All batches badge/url; isolated embeds still hydrate themselves.
if (! $this->backupMetaHydrated) {
$this->refreshBackupStatus();
}
}
#[On('refreshVolumeBackups')]
public function refreshBackupStatus(): void
{
$backup = $this->storage->scheduledBackups()->first();
$this->hasEnabledBackup = $backup?->enabled ?? false;
$this->backupUrl = null;
if (! $this->hasEnabledBackup || ! $this->resource instanceof Application) {
return;
}
$this->resource->loadMissing('environment.project');
$parameters = [
'project_uuid' => $this->resource->project()->uuid,
'environment_uuid' => $this->resource->environment->uuid,
'application_uuid' => $this->resource->uuid,
];
$hasOtherBackups = ScheduledVolumeBackup::query()
->forApplication($this->resource)
->where('id', '!=', $backup->id)
->exists();
$this->backupUrl = $hasOtherBackups
? route('project.application.backup.index', [...$parameters, 'search' => $this->storage->name])
: route('project.application.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]);
}
public function openBackupModal(): void
{
$this->authorize('update', $this->resource);
$this->showBackupModal = true;
}
#[On('modalClosed')]
public function onModalClosed(): void
{
// Drop the nested Create component from the DOM after close to free snapshot weight.
if ($this->showBackupModal) {
$this->showBackupModal = false;
}
}
public function instantSave(): void
{
$this->authorize('update', $this->resource);
$this->validate();
$this->syncData(true);
$this->storage->save();
$this->dispatch('success', 'Storage updated successfully');
}
public function submit()
{
$this->authorize('update', $this->resource);
$this->validate();
$this->syncData(true);
$this->storage->save();
$this->dispatch('success', 'Storage updated successfully');
}
public function delete($password, $selectedActions = [])
{
$this->authorize('update', $this->resource);
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
}
if ($this->storage->scheduledBackups()->exists()) {
$this->dispatch('error', 'Delete this volume backup schedule and its archives before deleting the volume.');
return false;
}
$this->storage->delete();
$this->dispatch('storageCountsChanged')->to(StorageComponent::class);
$this->dispatch('configurationChanged');
return true;
}
}
@@ -183,7 +183,7 @@
@if ($supportsPreviewSuffix)
<div class="volumes-col-pr min-w-0">
<span class="volumes-mobile-label volumes-field-label">PR suffix</span>
<x-forms.listbox id="forms.{{ $id }}.isPreviewSuffixEnabled" :options="[
<x-forms.listbox id="forms.{{ $id }}.isPreviewSuffixEnabled" portal :options="[
['value' => true, 'label' => 'Add suffix'],
['value' => false, 'label' => 'Share volume'],
]" />
@@ -1,156 +0,0 @@
@php
$showActionsColumn = $resource instanceof \App\Models\Application;
$gridClass = match (true) {
$supportsPreviewSuffix => 'volumes-table-grid-with-pr',
$showActionsColumn => 'volumes-table-grid',
default => 'volumes-table-grid-readonly',
};
$canUpdate = auth()->user()?->can('update', $resource) ?? false;
$inputsReadonly = $isReadOnly || ! $canUpdate;
$displayHostPath = filled($hostPath) ? $hostPath : '—';
@endphp
@if ($inputsReadonly)
{{-- Read-only: plain data-table row (service / compose / no permission) --}}
<div class="env-table-item" wire:key="storage-row-{{ $storage->id }}">
<div class="data-table-row {{ $gridClass }} text-[13px] text-neutral-700 dark:text-fg-dim">
<div class="volumes-cell-name min-w-0">
<span class="volumes-mobile-label volumes-field-label">Volume Name</span>
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 truncate text-[13px] font-medium text-neutral-950 dark:text-fg"
title="{{ $name }}">{{ $name }}</span>
@if ($hasEnabledBackup)
@if ($backupUrl)
<a href="{{ $backupUrl }}"
class="table-badge table-badge-success shrink-0 underline-offset-2 hover:underline"
title="Volume backup is enabled">
Backup
</a>
@else
<span class="table-badge table-badge-success shrink-0" title="Volume backup is enabled">
Backup
</span>
@endif
@endif
</div>
</div>
<div class="volumes-col-source min-w-0">
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
<span class="block min-w-0 truncate text-[13px]" title="{{ $hostPath }}">
{{ $displayHostPath }}
</span>
</div>
<div class="volumes-cell-dest min-w-0">
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
<span class="block min-w-0 truncate text-[13px] text-neutral-950 dark:text-fg"
title="{{ $mountPath }}">{{ $mountPath }}</span>
</div>
@if ($supportsPreviewSuffix)
<div class="volumes-col-pr min-w-0">
<span class="volumes-mobile-label volumes-field-label">PR suffix</span>
<span>{{ $isPreviewSuffixEnabled ? 'Add suffix' : 'Share volume' }}</span>
</div>
@endif
@if ($showActionsColumn)
<div class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
@if ($canUpdate)
@if ($showBackupModal)
<x-modal-input buttonTitle="Backup" title="Configure Volume Backup" :wireIgnore="false"
wireOpen="showBackupModal">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'volume:' . $storage->id"
wire:key="configure-readonly-volume-backup-{{ $storage->id }}" />
</x-modal-input>
@else
<x-forms.button type="button" wire:click="openBackupModal" class="!px-2.5 !text-xs">
Backup
</x-forms.button>
@endif
@else
<span class="text-neutral-400 dark:text-fg-faint"></span>
@endif
</div>
@endif
</div>
</div>
@else
{{-- Editable volume row --}}
<form wire:submit="submit" class="env-table-item" wire:key="storage-row-{{ $storage->id }}">
<div class="data-table-row {{ $gridClass }}">
<div class="volumes-cell-name min-w-0">
<span class="volumes-mobile-label volumes-field-label">Volume Name</span>
<div class="flex min-w-0 items-center gap-2">
<div class="min-w-0 flex-1">
<x-forms.input id="name" required />
</div>
@if ($hasEnabledBackup)
@if ($backupUrl)
<a href="{{ $backupUrl }}"
class="table-badge table-badge-success shrink-0 underline-offset-2 hover:underline"
title="Volume backup is enabled">
Backup
</a>
@else
<span class="table-badge table-badge-success shrink-0" title="Volume backup is enabled">
Backup
</span>
@endif
@endif
</div>
</div>
<div class="volumes-col-source min-w-0">
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
<x-forms.input id="hostPath" placeholder="Host path (optional)" />
</div>
<div class="volumes-cell-dest min-w-0">
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
<x-forms.input id="mountPath" required placeholder="/path/in/container" />
</div>
@if ($supportsPreviewSuffix)
<div class="volumes-col-pr min-w-0">
<span class="volumes-mobile-label volumes-field-label">PR suffix</span>
<x-forms.listbox id="isPreviewSuffixEnabled" onChange="instantSave" :options="[
['value' => true, 'label' => 'Add suffix'],
['value' => false, 'label' => 'Share volume'],
]" />
</div>
@endif
<div class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center justify-end gap-1.5">
<x-forms.button type="submit" class="!px-2.5 !text-xs">
Update
</x-forms.button>
@if ($resource instanceof \App\Models\Application)
@if ($showBackupModal)
<x-modal-input buttonTitle="Backup" title="Configure Volume Backup" :wireIgnore="false"
wireOpen="showBackupModal">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'volume:' . $storage->id"
wire:key="configure-volume-backup-{{ $storage->id }}" />
</x-modal-input>
@else
<x-forms.button type="button" wire:click="openBackupModal" class="!px-2.5 !text-xs">
Backup
</x-forms.button>
@endif
@endif
<x-modal-confirmation title="Confirm persistent storage deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="[
'The selected persistent storage/volume will be permanently deleted.',
'If the persistent storage/volume is actvily used by a resource data will be lost.',
]" confirmationText="{{ $storage->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
shortConfirmationLabel="Storage Name" />
</div>
</div>
</form>
@endif
+2 -3
View File
@@ -4,7 +4,6 @@ use App\Jobs\ServerStorageSaveJob;
use App\Livewire\Project\Service\FileStorage;
use App\Livewire\Project\Service\Storage;
use App\Livewire\Project\Shared\Storages\All;
use App\Livewire\Project\Shared\Storages\Show;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -247,8 +246,8 @@ test('deleting a volume mount refreshes the configuration warning', function ()
'resource_type' => $database->getMorphClass(),
]);
Livewire::test(Show::class, ['storage' => $volume, 'resource' => $database])
->call('delete', 'password')
Livewire::test(All::class, ['resource' => $database])
->call('delete', $volume->id, 'password')
->assertDispatched('configurationChanged');
expect($volume->fresh())->toBeNull();
@@ -91,7 +91,7 @@ function createPerfApplicationWithVolumes(int $volumeCount = 5): array
return [$application, $firstVolume, $team];
}
it('renders volume rows without nesting Livewire Show components', function () {
it('renders volume rows inline without nested Livewire row components', function () {
[$application] = createPerfApplicationWithVolumes(5);
$html = Livewire::test(All::class, ['resource' => $application])->html();
@@ -100,7 +100,6 @@ it('renders volume rows without nesting Livewire Show components', function () {
->toContain('data-table')
->toContain('openBackupModal')
->toContain('wire:submit="submit(')
->not->toContain('livewire:project.shared.storages.show')
->not->toContain('shared-configure-volume-backup-');
});
@@ -141,7 +141,6 @@ function createApplicationWithVolume(array $applicationAttributes = [], array $v
it('renders volumes as a data table with shared column headers', function () {
$allView = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
$showView = file_get_contents(resource_path('views/livewire/project/shared/storages/show.blade.php'));
$storageView = file_get_contents(resource_path('views/livewire/project/service/storage.blade.php'));
expect($allView)
@@ -159,17 +158,10 @@ it('renders volumes as a data table with shared column headers', function () {
->toContain('data-table-row')
->toContain('volumes-mobile-label')
->not->toContain('table-badge table-badge-success')
->not->toContain('livewire:project.shared.storages.show')
->not->toContain('x-status-badge')
->not->toContain('font-mono')
->not->toContain('Service volume mounts are read-only here.');
// Show remains available for isolated embeds/tests but is no longer nested from All.
expect($showView)
->toContain('data-table-row')
->toContain('volumes-table-grid')
->not->toContain('font-mono');
// Service stack page: one settings-section card per compose service/resource.
expect($storageView)
->toContain('Str::headline($resource->name)')
@@ -434,16 +426,13 @@ it('hides PR deployment suffix for databases', function () {
});
it('uses a compact table badge for enabled backups instead of status-badge', function () {
$showView = file_get_contents(resource_path('views/livewire/project/shared/storages/show.blade.php'));
$allView = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
expect($showView)
->toContain('table-badge-success')
expect($allView)
->toContain("'table-badge-success' => \$hasS3Backup")
->toContain('Volume backup is enabled')
->not->toContain('x-status-badge')
->not->toContain('status="Backup enabled"');
// Badge label is the short "Backup" text, not the old pill-with-label that broke the input row.
expect(preg_match('/table-badge-success[^>]*>\s*Backup\s*</', $showView))->toBeGreaterThan(0);
});
it('gates file storage PR suffix markup behind git_based applications', function () {
+10 -23
View File
@@ -8,7 +8,7 @@ use App\Livewire\Project\Application\Backup\Create as CreateScheduledVolumeBacku
use App\Livewire\Project\Service\FileStorage;
use App\Livewire\Project\Service\VolumeBackup\Create as CreateServiceVolumeBackup;
use App\Livewire\Project\Service\VolumeBackup\Index as ServiceVolumeBackupIndex;
use App\Livewire\Project\Shared\Storages\Show;
use App\Livewire\Project\Shared\Storages\All;
use App\Livewire\Project\Shared\Storages\VolumeBackups;
use App\Models\Application;
use App\Models\Environment;
@@ -483,11 +483,8 @@ it('shows the configure backup modal trigger inside the volume card instead of i
signInForVolumeBackups($this, $team);
[$application, $volume] = createVolumeBackupApplication($team);
$component = Livewire::test(Show::class, [
'storage' => $volume,
'resource' => $application,
])
->set('isReadOnly', true)
$component = Livewire::test(All::class, ['resource' => $application])
->set("forms.{$volume->id}.isReadOnly", true)
->assertSee('Backup')
->assertDontSee('Backups made while the application is writing');
@@ -497,6 +494,7 @@ it('shows the configure backup modal trigger inside the volume card instead of i
expect($html)
->toContain('Configure Volume Backup')
->toContain('data-table-row')
->not->toContain('wire:submit="submit('.$volume->id.')"')
->toContain('Backup');
});
@@ -512,10 +510,10 @@ it('only shows the backup enabled badge for an enabled volume backup', function
'enabled' => false,
]);
$component = Livewire::test(Show::class, [
'storage' => $volume,
'resource' => $application,
])->assertDontSee('table-badge-success', false);
$component = Livewire::test(All::class, ['resource' => $application])
->assertDontSee('Volume backup is enabled');
expect($component->get("volumeBackupMeta.{$volume->id}.enabled"))->toBeFalse();
$backup->update(['enabled' => true]);
@@ -528,17 +526,10 @@ it('only shows the backup enabled badge for an enabled volume backup', function
$component
->dispatch('refreshVolumeBackups')
->assertSee('table-badge-success', false)
->assertSee('Volume backup is enabled')
->assertSee('href="'.$backupUrl.'"', false);
Livewire::test(Show::class, [
'storage' => $volume,
'resource' => $application,
'isFirst' => false,
])
->assertSee('table-badge-success', false)
->assertSee('Volume backup is enabled');
expect($component->get("volumeBackupMeta.{$volume->id}.url"))->toBe($backupUrl);
});
it('links the backup enabled badge to a filtered backup list when the application has multiple schedules', function () {
@@ -564,11 +555,7 @@ it('links the backup enabled badge to a filtered backup list when the applicatio
'search' => $volume->name,
]);
Livewire::test(Show::class, [
'storage' => $volume,
'resource' => $application,
])
->assertSee('table-badge-success', false)
Livewire::test(All::class, ['resource' => $application])
->assertSee('Volume backup is enabled')
->assertSee('href="'.$backupUrl.'"', false);
});