feat: implement storage quota checks and VM state validation for drive management

This commit is contained in:
2025-12-05 06:47:33 -05:00
parent 2a07fe78fc
commit 61b74aa0a1
2 changed files with 265 additions and 36 deletions

View File

@@ -32,6 +32,10 @@
}
</style>
@php
$vmRunning = $status && $status->status !== 'stopped';
@endphp
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2">
@@ -96,6 +100,21 @@
<p class="text-sm text-base/60">Manage virtual hard disks attached to your VPS. Add, configure, or remove storage drives.</p>
</div>
<!-- VM Running Warning -->
@if($status && $status->status !== 'stopped')
<div class="bg-danger/10 border border-danger/30 rounded-lg p-4">
<div class="flex items-center gap-3">
<div class="flex-shrink-0">
<x-ri-error-warning-line class="size-5 text-danger" />
</div>
<div>
<h4 class="text-sm font-semibold text-danger">Server Must Be Powered Off</h4>
<p class="text-sm text-danger/80 mt-0.5">Storage changes can only be made when the server is in a powered off state. Please shut down your server before making any storage modifications.</p>
</div>
</div>
</div>
@endif
<!-- Storage Usage Overview -->
<div class="bg-background-secondary border border-neutral rounded-lg p-5">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
@@ -114,12 +133,19 @@
<x-ri-refresh-line class="size-4" wire:loading.class="animate-spin" wire:target="loadStorageConfiguration" />
Refresh
</button>
@if($this->remainingStorage > 0)
@if($this->remainingStorage > 0 && !$vmRunning)
<button wire:click="openAddDriveModal"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
<x-ri-add-line class="size-4" />
Add Drive
</button>
@elseif($this->remainingStorage > 0 && $vmRunning)
<button disabled
title="Server must be powered off to add drives"
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-neutral/50 text-base/50 cursor-not-allowed">
<x-ri-add-line class="size-4" />
Add Drive
</button>
@endif
</div>
</div>
@@ -211,14 +237,14 @@
<button wire:click="saveScsiController"
wire:loading.attr="disabled"
class="inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
@if($this->scsiControllerSynced) disabled @endif>
@if($this->scsiControllerSynced || $vmRunning) disabled @endif>
<x-loading target="saveScsiController" />
<span wire:loading.remove wire:target="saveScsiController">Save Changes</span>
<span wire:loading wire:target="saveScsiController">Saving...</span>
</button>
</div>
@if($status && $status->status !== 'stopped' && !$this->scsiControllerSynced)
@if($vmRunning && !$this->scsiControllerSynced)
<div class="flex items-center gap-2 p-3 rounded-lg bg-warning/10 border border-warning/30 text-warning text-sm">
<x-ri-alert-line class="size-4 flex-shrink-0" />
<span>VM must be stopped to change the SCSI controller type.</span>
@@ -237,10 +263,12 @@
<div class="p-8 text-center">
<x-ri-hard-drive-2-line class="size-12 mx-auto text-base/30 mb-3" />
<p class="text-base/60">No drives attached to this VM.</p>
@if($this->remainingStorage > 0)
@if($this->remainingStorage > 0 && !$vmRunning)
<button wire:click="openAddDriveModal" class="mt-4 text-primary hover:text-primary/80 text-sm font-medium">
Add your first drive →
</button>
@elseif($this->remainingStorage > 0 && $vmRunning)
<p class="mt-4 text-base/50 text-sm">Power off server to add drives</p>
@endif
</div>
@else
@@ -275,37 +303,64 @@
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<button wire:click="openEditDriveModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
<x-ri-settings-3-line class="size-3.5" />
Settings
</button>
<button wire:click="openMoveDriveModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
<x-ri-arrow-left-right-line class="size-3.5" />
Device Bus
</button>
@if(count($unusedDrives) >= 2)
@if($vmRunning)
<button disabled
title="Maximum of 2 detached drives allowed"
title="Server must be powered off"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral/30 bg-neutral/10 text-base/50 cursor-not-allowed">
<x-ri-settings-3-line class="size-3.5" />
Settings
</button>
<button disabled
title="Server must be powered off"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral/30 bg-neutral/10 text-base/50 cursor-not-allowed">
<x-ri-arrow-left-right-line class="size-3.5" />
Device Bus
</button>
<button disabled
title="Server must be powered off"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(150 150 150 / 0.3); background-color: rgb(150 150 150 / 0.1); color: rgb(150 150 150); cursor: not-allowed; opacity: 0.6;">
<x-ri-eject-line class="size-3.5" />
Detach
</button>
<button disabled
title="Server must be powered off"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral/30 bg-neutral/10 text-base/50 cursor-not-allowed">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
@else
<button wire:click="openDetachModal('{{ $driveKey }}')"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(234 179 8 / 0.5); background-color: rgb(234 179 8 / 0.1); color: rgb(234 179 8); transition: background-color 0.15s ease;"
onmouseover="this.style.backgroundColor='rgb(234 179 8 / 0.2)'"
onmouseout="this.style.backgroundColor='rgb(234 179 8 / 0.1)'">
<x-ri-eject-line class="size-3.5" />
Detach
<button wire:click="openEditDriveModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
<x-ri-settings-3-line class="size-3.5" />
Settings
</button>
<button wire:click="openMoveDriveModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
<x-ri-arrow-left-right-line class="size-3.5" />
Device Bus
</button>
@if(count($unusedDrives) >= 2)
<button disabled
title="Maximum of 2 detached drives allowed"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(150 150 150 / 0.3); background-color: rgb(150 150 150 / 0.1); color: rgb(150 150 150); cursor: not-allowed; opacity: 0.6;">
<x-ri-eject-line class="size-3.5" />
Detach
</button>
@else
<button wire:click="openDetachModal('{{ $driveKey }}')"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(234 179 8 / 0.5); background-color: rgb(234 179 8 / 0.1); color: rgb(234 179 8); transition: background-color 0.15s ease;"
onmouseover="this.style.backgroundColor='rgb(234 179 8 / 0.2)'"
onmouseout="this.style.backgroundColor='rgb(234 179 8 / 0.1)'">
<x-ri-eject-line class="size-3.5" />
Detach
</button>
@endif
<button wire:click="openDeleteModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-danger/50 bg-danger/10 text-danger hover:bg-danger/20 transition-colors">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
@endif
<button wire:click="openDeleteModal('{{ $driveKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-danger/50 bg-danger/10 text-danger hover:bg-danger/20 transition-colors">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
</div>
</div>
</div>
@@ -372,28 +427,49 @@
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@if($totalAllocatedStorage >= $maxAllowedStorage && $maxAllowedStorage > 0)
@if($vmRunning)
<button disabled
title="Server must be powered off"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(150 150 150 / 0.3); background-color: rgb(150 150 150 / 0.1); color: rgb(150 150 150); cursor: not-allowed; opacity: 0.6;">
<x-ri-link class="size-3.5" />
Reattach
</button>
<button disabled
title="Server must be powered off"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(150 150 150 / 0.3); background-color: rgb(150 150 150 / 0.1); color: rgb(150 150 150); cursor: not-allowed; opacity: 0.6;">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
@elseif($totalAllocatedStorage >= $maxAllowedStorage && $maxAllowedStorage > 0)
<button disabled
title="Storage quota reached - please upgrade your service or delete drives"
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(150 150 150 / 0.3); background-color: rgb(150 150 150 / 0.1); color: rgb(150 150 150); cursor: not-allowed; opacity: 0.6;">
<x-ri-link class="size-3.5" />
Reattach
</button>
<button wire:click="deleteUnusedDrive('{{ $unusedKey }}')"
wire:confirm="Are you sure you want to permanently delete this detached drive? This action cannot be undone."
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(239 68 68 / 0.5); background-color: rgb(239 68 68 / 0.1); color: rgb(239 68 68); transition: background-color 0.15s ease;"
onmouseover="this.style.backgroundColor='rgb(239 68 68 / 0.2)'"
onmouseout="this.style.backgroundColor='rgb(239 68 68 / 0.1)'">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
@else
<button wire:click="openReattachModal('{{ $unusedKey }}')"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-success/50 bg-success/10 text-success hover:bg-success/20 transition-colors">
<x-ri-link class="size-3.5" />
Reattach
</button>
<button wire:click="deleteUnusedDrive('{{ $unusedKey }}')"
wire:confirm="Are you sure you want to permanently delete this detached drive? This action cannot be undone."
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(239 68 68 / 0.5); background-color: rgb(239 68 68 / 0.1); color: rgb(239 68 68); transition: background-color 0.15s ease;"
onmouseover="this.style.backgroundColor='rgb(239 68 68 / 0.2)'"
onmouseout="this.style.backgroundColor='rgb(239 68 68 / 0.1)'">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
@endif
<button wire:click="deleteUnusedDrive('{{ $unusedKey }}')"
wire:confirm="Are you sure you want to permanently delete this detached drive? This action cannot be undone."
style="display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.5rem; border: 1px solid rgb(239 68 68 / 0.5); background-color: rgb(239 68 68 / 0.1); color: rgb(239 68 68); transition: background-color 0.15s ease;"
onmouseover="this.style.backgroundColor='rgb(239 68 68 / 0.2)'"
onmouseout="this.style.backgroundColor='rgb(239 68 68 / 0.1)'">
<x-ri-delete-bin-line class="size-3.5" />
Delete
</button>
</div>
</div>
</div>