- Updated serverplaceholder.blade.php to enhance loading skeletons for server details. - Added navigation breadcrumbs to statistics, subdomain, and system views for better user experience. - Improved loading indicators for CPU, network, memory, and disk charts in statistics.blade.php. - Refined storage.blade.php layout with section dividers and improved drive display, including bus type indicators and quota usage. - Enhanced VNC and Xterm popups with a new header design, loading animations, and reconnect functionality. - Added error handling for VNC and Xterm connections to inform users when unable to connect.
1050 lines
74 KiB
PHP
1050 lines
74 KiB
PHP
<x-proxmox::layout :server="$server" view="storage" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
@php
|
|
$vmRunning = $status && $status->status !== 'stopped';
|
|
@endphp
|
|
|
|
<div class="space-y-6">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col gap-2">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<h2 class="text-xl font-semibold break-words">Storage Management</h2>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<!-- Modal Backdrop -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<!-- Modal -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.self="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-hard-drive-2-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Storage Management
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Manage the storage drives attached to your VPS. You can add, remove, and configure drives with different bus types, cache modes, and performance options.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Bus Types:</span> SCSI (VirtIO SCSI) offers the best performance with IO thread support. VirtIO Block is also high-performance. IDE and SATA are available for compatibility.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Important:</span> The VM must be stopped to add, remove, or move drives. Total storage cannot exceed your plan allocation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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-error/10 border border-error/30 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<x-ri-error-warning-fill class="size-5 text-error flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<h3 class="text-sm font-semibold">Server Must Be Powered Off</h3>
|
|
<p class="text-xs text-base/70 leading-relaxed mt-1">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 Over-Provisioned Warning -->
|
|
@if($totalAllocatedStorage > $maxAllowedStorage && $maxAllowedStorage > 0)
|
|
<div class="bg-error/10 border border-error/30 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<x-ri-alarm-warning-fill class="size-5 text-error flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<h3 class="text-sm font-semibold">Storage Quota Exceeded</h3>
|
|
<p class="text-xs text-base/70 leading-relaxed mt-1">
|
|
<strong>Your server has {{ number_format($totalAllocatedStorage, 1) }} GB of storage configured, but your plan only allows {{ number_format($maxAllowedStorage, 1) }} GB.</strong>
|
|
The server cannot be started until storage is reduced to within your plan limits. Please delete drives or upgrade your service to allow this configuration.
|
|
</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">
|
|
<div>
|
|
<h3 class="text-lg font-semibold">Storage Quota</h3>
|
|
<p class="text-sm text-base/60 mt-1">
|
|
{{ number_format($totalAllocatedStorage, 1) }} GB used of {{ number_format($maxAllowedStorage, 1) }} GB allocated
|
|
@if(count($unusedDrives) > 0)
|
|
<span class="text-warning">(includes detached drives)</span>
|
|
@endif
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button wire:click="loadStorageConfiguration"
|
|
class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
<x-ri-refresh-line class="size-4" wire:loading.class="animate-spin" wire:target="loadStorageConfiguration" />
|
|
Refresh
|
|
</button>
|
|
@if($this->remainingStorage > 0 && !$vmRunning)
|
|
<button wire:click="openAddDriveModal"
|
|
wire:loading.attr="disabled"
|
|
wire:loading.class="opacity-75"
|
|
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-hourglass-fill class="size-4 animate-pulse" wire:loading wire:target="openAddDriveModal" />
|
|
<x-ri-add-line class="size-4" wire:loading.remove wire:target="openAddDriveModal" />
|
|
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>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="w-full bg-neutral/30 rounded-full h-3 overflow-hidden">
|
|
@php
|
|
$usagePercent = $maxAllowedStorage > 0 ? min(100, ($totalAllocatedStorage / $maxAllowedStorage) * 100) : 0;
|
|
$colorClass = $usagePercent < 70 ? 'bg-success' : ($usagePercent < 90 ? 'bg-warning' : 'bg-danger');
|
|
@endphp
|
|
<div class="{{ $colorClass }} h-3 rounded-full transition-all duration-500" style="width: {{ $usagePercent }}%"></div>
|
|
</div>
|
|
<div class="flex justify-between mt-2 text-xs text-base/50">
|
|
<span>{{ number_format($this->remainingStorage, 1) }} GB remaining</span>
|
|
<span>{{ number_format($usagePercent, 1) }}% used</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section Divider -->
|
|
<div class="border-t border-neutral/20"></div>
|
|
|
|
<!-- SCSI Controller Configuration -->
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<h3 class="text-lg font-semibold">SCSI Controller</h3>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<!-- Modal Backdrop -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<!-- Modal -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.self="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-cpu-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
SCSI Controller Types
|
|
</h6>
|
|
</div>
|
|
<div class="space-y-3 text-sm text-base/90">
|
|
<p><strong>VirtIO SCSI / VirtIO SCSI Single:</strong> Best performance with paravirtualized drivers. Recommended for Linux and modern Windows.</p>
|
|
<p><strong>LSI 53C895A / LSI 53C810:</strong> Emulated LSI controllers with wide compatibility.</p>
|
|
<p><strong>MegaRAID SAS 8708EM2:</strong> Emulated MegaRAID controller.</p>
|
|
<p><strong>VMware PVSCSI:</strong> VMware paravirtualized controller, useful for VMs migrated from VMware.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-base/60 mt-1">Select the SCSI controller type for SCSI drives.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<div class="flex-1">
|
|
<select wire:model.live="scsiControllerType"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($scsiControllerOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<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 || $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($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>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Section Divider -->
|
|
<div class="border-t border-neutral/20"></div>
|
|
|
|
<!-- Drives List -->
|
|
<div class="bg-background-secondary border border-neutral rounded-lg overflow-hidden">
|
|
<div class="p-5 border-b border-neutral">
|
|
<h3 class="text-lg font-semibold">Attached Drives</h3>
|
|
<p class="text-sm text-base/60 mt-1">Manage the virtual hard disks connected to your VPS.</p>
|
|
</div>
|
|
|
|
@if(count($drives) === 0)
|
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
<div class="flex items-center justify-center w-16 h-16 rounded-2xl bg-base/5 mb-4">
|
|
<x-ri-hard-drive-2-line class="size-8 text-base/20" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-base/70 mb-1">No drives attached</h3>
|
|
<p class="text-xs text-base/50 max-w-sm">Virtual hard disks store your operating system, applications, and data. Add a drive to get started.</p>
|
|
@if($this->remainingStorage > 0 && !$vmRunning)
|
|
<button wire:click="openAddDriveModal" class="mt-4 inline-flex items-center gap-1.5 text-primary hover:text-primary/80 text-sm font-medium transition-colors">
|
|
<x-ri-add-line class="size-4" />
|
|
Add your first drive
|
|
</button>
|
|
@elseif($this->remainingStorage > 0 && $vmRunning)
|
|
<p class="mt-3 text-base/50 text-xs">Power off server to add drives</p>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<div class="divide-y divide-neutral">
|
|
@foreach($drives as $driveKey => $drive)
|
|
<div wire:key="drive-{{ $driveKey }}" class="group p-4 hover:bg-background/50 transition-all duration-300 hover:shadow-sm">
|
|
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 flex-shrink-0">
|
|
@if($drive['bus'] === 'scsi')
|
|
<x-ri-database-2-line class="size-5 text-primary" />
|
|
@elseif($drive['bus'] === 'virtio')
|
|
<x-ri-speed-line class="size-5 text-primary" />
|
|
@elseif($drive['bus'] === 'sata')
|
|
<x-ri-hard-drive-2-fill class="size-5 text-primary" />
|
|
@elseif($drive['bus'] === 'ide')
|
|
<x-ri-hard-drive-line class="size-5 text-primary" />
|
|
@else
|
|
<x-ri-hard-drive-2-fill class="size-5 text-primary" />
|
|
@endif
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<h4 class="font-semibold text-base">{{ $drive['label'] }}</h4>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-neutral/30 text-base/70">{{ $driveKey }}</span>
|
|
@php
|
|
$busLabel = $busTypes[$drive['bus']]['label'] ?? strtoupper($drive['bus']);
|
|
$busColor = match($drive['bus']) {
|
|
'scsi' => 'bg-primary/15 text-primary',
|
|
'virtio' => 'bg-success/15 text-success',
|
|
'sata' => 'bg-info/15 text-info',
|
|
'ide' => 'bg-warning/15 text-warning',
|
|
default => 'bg-neutral/20 text-base/70',
|
|
};
|
|
@endphp
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded {{ $busColor }}">{{ $busLabel }}</span>
|
|
@if($drive['ssd'])
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-success/20 text-success">SSD</span>
|
|
@endif
|
|
@if($drive['iothread'])
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-info/20 text-info">IO Thread</span>
|
|
@endif
|
|
</div>
|
|
<!-- Drive size relative to quota -->
|
|
@if($maxAllowedStorage > 0)
|
|
<div class="w-full max-w-[200px] mt-2">
|
|
<div class="w-full bg-neutral/20 rounded-full h-1.5 overflow-hidden">
|
|
@php
|
|
$drivePercent = min(100, ($drive['size'] / $maxAllowedStorage) * 100);
|
|
@endphp
|
|
<div class="bg-primary/60 h-1.5 rounded-full transition-all duration-500" style="width: {{ $drivePercent }}%"></div>
|
|
</div>
|
|
<p class="text-[10px] text-base/40 mt-0.5">{{ number_format($drivePercent, 1) }}% of quota</p>
|
|
</div>
|
|
@endif
|
|
<div class="flex flex-wrap gap-2 mt-2">
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-neutral/20 text-foreground-secondary">{{ number_format($drive['size'], 1) }} GB</span>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-neutral/20 text-foreground-secondary">Cache: {{ ucfirst($drive['cache']) }}</span>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-neutral/20 text-foreground-secondary">AIO: {{ $drive['aio'] }}</span>
|
|
@if($drive['discard'])
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-success/20 text-success">Discard</span>
|
|
@endif
|
|
</div>
|
|
<!-- Hover-reveal extra details -->
|
|
<div class="opacity-0 group-hover:opacity-100 max-h-0 group-hover:max-h-40 transition-all duration-300 overflow-hidden">
|
|
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-2 pt-2 border-t border-neutral/10 text-[11px] text-base/50">
|
|
<span>Bus: <strong class="text-base/70">{{ strtoupper($drive['bus']) }}{{ $drive['bus_index'] }}</strong></span>
|
|
<span>SSD Emulation: <strong class="text-base/70">{{ $drive['ssd'] ? 'Yes' : 'No' }}</strong></span>
|
|
<span>IO Thread: <strong class="text-base/70">{{ $drive['iothread'] ? 'Enabled' : 'Disabled' }}</strong></span>
|
|
<span>Discard: <strong class="text-base/70">{{ $drive['discard'] ? 'On' : 'Off' }}</strong></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
@if($vmRunning)
|
|
<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-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"
|
|
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-neutral cursor-not-allowed opacity-60">
|
|
<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="openEditDriveModal('{{ $driveKey }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:target="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-hourglass-fill class="size-3.5 animate-pulse" wire:loading wire:target="openEditDriveModal('{{ $driveKey }}')" />
|
|
<x-ri-settings-3-line class="size-3.5" wire:loading.remove wire:target="openEditDriveModal('{{ $driveKey }}')" />
|
|
Settings
|
|
</button>
|
|
<button wire:click="openMoveDriveModal('{{ $driveKey }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:target="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-hourglass-fill class="size-3.5 animate-pulse" wire:loading wire:target="openMoveDriveModal('{{ $driveKey }}')" />
|
|
<x-ri-arrow-left-right-line class="size-3.5" wire:loading.remove wire:target="openMoveDriveModal('{{ $driveKey }}')" />
|
|
Device Bus
|
|
</button>
|
|
@if(count($unusedDrives) >= 2)
|
|
<button disabled
|
|
title="Maximum of 2 detached drives allowed"
|
|
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-neutral cursor-not-allowed opacity-60">
|
|
<x-ri-eject-line class="size-3.5" />
|
|
Detach
|
|
</button>
|
|
@else
|
|
<button wire:click="openDetachModal('{{ $driveKey }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:target="openDetachModal('{{ $driveKey }}')"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-warning/50 bg-warning/10 text-warning hover:bg-warning/20 transition-colors">
|
|
<x-ri-hourglass-fill class="size-3.5 animate-pulse" wire:loading wire:target="openDetachModal('{{ $driveKey }}')" />
|
|
<x-ri-eject-line class="size-3.5" wire:loading.remove wire:target="openDetachModal('{{ $driveKey }}')" />
|
|
Detach
|
|
</button>
|
|
@endif
|
|
<button wire:click="openDeleteModal('{{ $driveKey }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:target="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-hourglass-fill class="size-3.5 animate-pulse" wire:loading wire:target="openDeleteModal('{{ $driveKey }}')" />
|
|
<x-ri-delete-bin-line class="size-3.5" wire:loading.remove wire:target="openDeleteModal('{{ $driveKey }}')" />
|
|
Delete
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Detached/Unused Drives -->
|
|
@if(count($unusedDrives) > 0)
|
|
<div class="bg-background-secondary border border-neutral rounded-lg overflow-hidden">
|
|
<div class="p-5 border-b border-neutral">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-eject-line class="size-5 text-warning" />
|
|
<h3 class="text-lg font-semibold">Detached Drives</h3>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded {{ count($unusedDrives) >= 2 ? 'bg-danger/20 text-danger' : 'bg-warning/20 text-warning' }}">{{ count($unusedDrives) }}/2</span>
|
|
</div>
|
|
<p class="text-sm text-base/60 mt-1">These drives have been detached but their data is preserved. You can reattach them or delete them permanently.</p>
|
|
</div>
|
|
</div>
|
|
@if(count($unusedDrives) >= 2)
|
|
<div class="mt-3 p-3 rounded-lg bg-danger/10 border border-danger/30">
|
|
<p class="text-xs text-danger flex items-center gap-2">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
|
Maximum detached drives reached. Please reattach or delete drives before detaching more.
|
|
</p>
|
|
</div>
|
|
@endif
|
|
@if($totalAllocatedStorage >= $maxAllowedStorage && $maxAllowedStorage > 0)
|
|
<div class="mt-3 p-3 rounded-lg bg-danger/10 border border-danger/30">
|
|
<p class="text-xs text-danger flex items-center gap-2">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
|
Storage quota reached ({{ number_format($totalAllocatedStorage, 1) }} GB / {{ number_format($maxAllowedStorage, 1) }} GB). Reattaching drives is disabled. Please upgrade your service or delete drives to allow this configuration.
|
|
</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="divide-y divide-neutral">
|
|
@foreach($unusedDrives as $unusedKey => $unusedDrive)
|
|
<div wire:key="unused-{{ $unusedKey }}" class="p-4 hover:bg-background/50 transition-colors">
|
|
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
|
<div class="flex items-start gap-4">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-warning/10 flex-shrink-0">
|
|
<x-ri-hard-drive-2-line class="size-5 text-warning" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<h4 class="font-semibold text-base">{{ $unusedDrive['label'] }}</h4>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-warning/20 text-warning">{{ $unusedKey }}</span>
|
|
@if(isset($unusedDrive['size_formatted']))
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded bg-info/20 text-info">{{ $unusedDrive['size_formatted'] }}</span>
|
|
@endif
|
|
</div>
|
|
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-1 text-xs text-base/60">
|
|
<span class="truncate max-w-[200px]" title="{{ $unusedDrive['volume'] }}">{{ $unusedDrive['volume'] }}</span>
|
|
@if(isset($unusedDrive['storage']))
|
|
<span>Storage: {{ $unusedDrive['storage'] }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
@if($vmRunning)
|
|
<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-neutral cursor-not-allowed opacity-60">
|
|
<x-ri-link class="size-3.5" />
|
|
Reattach
|
|
</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-neutral cursor-not-allowed opacity-60">
|
|
<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"
|
|
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-neutral cursor-not-allowed opacity-60">
|
|
<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."
|
|
wire:loading.attr="disabled"
|
|
wire:target="deleteUnusedDrive('{{ $unusedKey }}')"
|
|
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-hourglass-fill wire:loading wire:target="deleteUnusedDrive('{{ $unusedKey }}')" class="size-3.5 animate-pulse" />
|
|
<x-ri-delete-bin-line wire:loading.remove wire:target="deleteUnusedDrive('{{ $unusedKey }}')" class="size-3.5" />
|
|
Delete
|
|
</button>
|
|
@else
|
|
<button wire:click="openReattachModal('{{ $unusedKey }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:target="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 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<x-ri-hourglass-fill wire:loading wire:target="openReattachModal('{{ $unusedKey }}')" class="size-3.5 animate-pulse" />
|
|
<x-ri-link wire:loading.remove wire:target="openReattachModal('{{ $unusedKey }}')" 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."
|
|
wire:loading.attr="disabled"
|
|
wire:target="deleteUnusedDrive('{{ $unusedKey }}')"
|
|
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-hourglass-fill wire:loading wire:target="deleteUnusedDrive('{{ $unusedKey }}')" class="size-3.5 animate-pulse" />
|
|
<x-ri-delete-bin-line wire:loading.remove wire:target="deleteUnusedDrive('{{ $unusedKey }}')" class="size-3.5" />
|
|
Delete
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Add Drive Modal -->
|
|
@if($showAddDriveModal)
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeAddDriveModal()" x-init="$el.querySelector('input')?.focus()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" wire:click="closeAddDriveModal"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<h3 class="text-lg font-semibold">Add New Drive</h3>
|
|
<button wire:click="closeAddDriveModal" class="text-base/50 hover:text-base transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<!-- Bus Type -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Bus/Device Type</label>
|
|
<select wire:model.live="newDriveBus"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($busTypes as $busKey => $busConfig)
|
|
<option value="{{ $busKey }}">{{ $busConfig['label'] }} (max {{ $busConfig['max'] }} drives)</option>
|
|
@endforeach
|
|
</select>
|
|
<p class="text-xs text-base/50 mt-1">
|
|
@if($newDriveBus === 'scsi')
|
|
SCSI provides best performance with IO thread support. Recommended for most use cases.
|
|
@elseif($newDriveBus === 'virtio')
|
|
VirtIO Block offers high performance but does not support SSD emulation.
|
|
@elseif($newDriveBus === 'sata')
|
|
SATA provides good compatibility with most operating systems.
|
|
@else
|
|
IDE is primarily for legacy compatibility.
|
|
@endif
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Size -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Size (GB)</label>
|
|
<input type="number"
|
|
wire:model="newDriveSize"
|
|
min="1"
|
|
max="{{ $this->remainingStorage }}"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
<p class="text-xs text-base/50 mt-1">Maximum available: {{ number_format($this->remainingStorage, 1) }} GB</p>
|
|
</div>
|
|
|
|
<!-- Cache Mode -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Cache Mode</label>
|
|
<select wire:model="newDriveCache"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($cacheOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Async IO -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Async IO Type</label>
|
|
<select wire:model="newDriveAio"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($aioOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Options Row -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<!-- SSD Emulation -->
|
|
<label class="flex items-center gap-2 cursor-pointer {{ !$this->newDriveSsdSupported ? 'opacity-50' : '' }}">
|
|
<input type="checkbox"
|
|
wire:model="newDriveSsd"
|
|
class="rounded border-neutral text-primary focus:ring-primary"
|
|
@if(!$this->newDriveSsdSupported) disabled @endif>
|
|
<span class="text-sm">SSD Emulation</span>
|
|
</label>
|
|
|
|
<!-- IO Thread -->
|
|
<label class="flex items-center gap-2 cursor-pointer {{ !$this->newDriveIothreadSupported ? 'opacity-50' : '' }}">
|
|
<input type="checkbox"
|
|
wire:model="newDriveIothread"
|
|
class="rounded border-neutral text-primary focus:ring-primary"
|
|
@if(!$this->newDriveIothreadSupported) disabled @endif>
|
|
<span class="text-sm">IO Thread</span>
|
|
</label>
|
|
|
|
<!-- Discard/TRIM -->
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox"
|
|
wire:model="newDriveDiscard"
|
|
class="rounded border-neutral text-primary focus:ring-primary">
|
|
<span class="text-sm">Discard (TRIM)</span>
|
|
</label>
|
|
</div>
|
|
|
|
@if($status && $status->status !== 'stopped')
|
|
<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 add new drives.</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeAddDriveModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="addDrive"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
@if($status && $status->status !== 'stopped') disabled @endif>
|
|
<x-loading target="addDrive" />
|
|
<span wire:loading.remove wire:target="addDrive">Add Drive</span>
|
|
<span wire:loading wire:target="addDrive">Adding...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Edit Drive Modal -->
|
|
@if($showEditDriveModal && $editingDriveKey && isset($drives[$editingDriveKey]))
|
|
@php $editDrive = $drives[$editingDriveKey]; @endphp
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeEditDriveModal()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" wire:click="closeEditDriveModal"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<h3 class="text-lg font-semibold">Edit Drive Settings - {{ $editDrive['label'] }}</h3>
|
|
<button wire:click="closeEditDriveModal" class="text-base/50 hover:text-base transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div class="p-3 rounded-lg bg-neutral/20 text-sm">
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div><span class="text-base/50">Device:</span> {{ $editingDriveKey }}</div>
|
|
<div><span class="text-base/50">Size:</span> {{ number_format($editDrive['size'], 1) }} GB</div>
|
|
<div><span class="text-base/50">Bus Type:</span> {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cache Mode -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Cache Mode</label>
|
|
<select wire:model="editDriveCache"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($cacheOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Async IO -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Async IO Type</label>
|
|
<select wire:model="editDriveAio"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($aioOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Options Row -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<!-- SSD Emulation -->
|
|
<label class="flex items-center gap-2 cursor-pointer {{ !$this->editDriveSsdSupported ? 'opacity-50' : '' }}">
|
|
<input type="checkbox"
|
|
wire:model="editDriveSsd"
|
|
class="rounded border-neutral text-primary focus:ring-primary"
|
|
@if(!$this->editDriveSsdSupported) disabled @endif>
|
|
<span class="text-sm">SSD Emulation</span>
|
|
</label>
|
|
|
|
<!-- IO Thread -->
|
|
<label class="flex items-center gap-2 cursor-pointer {{ !$this->editDriveIothreadSupported ? 'opacity-50' : '' }}">
|
|
<input type="checkbox"
|
|
wire:model="editDriveIothread"
|
|
class="rounded border-neutral text-primary focus:ring-primary"
|
|
@if(!$this->editDriveIothreadSupported) disabled @endif>
|
|
<span class="text-sm">IO Thread</span>
|
|
</label>
|
|
|
|
<!-- Discard/TRIM -->
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox"
|
|
wire:model="editDriveDiscard"
|
|
class="rounded border-neutral text-primary focus:ring-primary">
|
|
<span class="text-sm">Discard (TRIM)</span>
|
|
</label>
|
|
</div>
|
|
|
|
@if(!$this->editDriveSsdSupported)
|
|
<p class="text-xs text-base/50">Note: SSD emulation is not available for {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }} drives.</p>
|
|
@endif
|
|
|
|
@if(!$this->editDriveIothreadSupported)
|
|
<p class="text-xs text-base/50">Note: IO Thread is not available for {{ $busTypes[$editDrive['bus']]['label'] ?? ucfirst($editDrive['bus']) }} drives.</p>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeEditDriveModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="saveDriveSettings"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 transition-colors">
|
|
<x-loading target="saveDriveSettings" />
|
|
<span wire:loading.remove wire:target="saveDriveSettings">Save Changes</span>
|
|
<span wire:loading wire:target="saveDriveSettings">Saving...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Change Device Bus Modal -->
|
|
@if($showMoveDriveModal && $movingDriveKey && isset($drives[$movingDriveKey]))
|
|
@php $moveDrive = $drives[$movingDriveKey]; @endphp
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeMoveDriveModal()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" wire:click="closeMoveDriveModal"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<h3 class="text-lg font-semibold">Change Device Bus - {{ $moveDrive['label'] }}</h3>
|
|
<button wire:click="closeMoveDriveModal" class="text-base/50 hover:text-base transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div class="p-3 rounded-lg bg-neutral/20 text-sm">
|
|
<p><span class="text-base/50">Current Device:</span> {{ $movingDriveKey }} ({{ $busTypes[$moveDrive['bus']]['label'] ?? ucfirst($moveDrive['bus']) }})</p>
|
|
<p class="mt-1"><span class="text-base/50">Size:</span> {{ number_format($moveDrive['size'], 1) }} GB</p>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-info/10 border border-info/30 text-info text-sm">
|
|
<x-ri-information-line class="size-4 flex-shrink-0" />
|
|
<span>Moving a drive changes its bus type. This may require reinstalling drivers in the guest OS.</span>
|
|
</div>
|
|
|
|
<!-- Target Bus Type -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Target Bus Type</label>
|
|
<select wire:model="moveDriveTargetBus"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-neutral bg-background focus:border-primary focus:ring-1 focus:ring-primary transition-colors">
|
|
@foreach($busTypes as $busKey => $busConfig)
|
|
<option value="{{ $busKey }}" @if($moveDrive['bus'] === $busKey) disabled @endif>
|
|
{{ $busConfig['label'] }} @if($moveDrive['bus'] === $busKey) (Current) @endif
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
@if($status && $status->status !== 'stopped')
|
|
<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 device bus.</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeMoveDriveModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="moveDrive"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
@if(($status && $status->status !== 'stopped') || $moveDrive['bus'] === $moveDriveTargetBus) disabled @endif>
|
|
<x-loading target="moveDrive" />
|
|
<span wire:loading.remove wire:target="moveDrive">Change Bus</span>
|
|
<span wire:loading wire:target="moveDrive">Changing...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Detach Drive Confirmation Modal -->
|
|
@if($showDetachModal && $detachingDriveKey && isset($drives[$detachingDriveKey]))
|
|
@php $detachDrive = $drives[$detachingDriveKey]; @endphp
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeDetachModal()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" wire:click="closeDetachModal"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-warning/10">
|
|
<x-ri-eject-line class="size-5 text-warning" />
|
|
</div>
|
|
<h3 class="text-lg font-semibold">Detach Drive</h3>
|
|
</div>
|
|
<button wire:click="closeDetachModal" class="text-base/50 hover:text-base transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div class="p-3 rounded-lg bg-neutral/20 text-sm">
|
|
<p><span class="text-base/50">Drive:</span> {{ $detachDrive['label'] }} ({{ $detachingDriveKey }})</p>
|
|
<p class="mt-1"><span class="text-base/50">Size:</span> {{ number_format($detachDrive['size'], 1) }} GB</p>
|
|
</div>
|
|
|
|
<div class="flex items-start 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 mt-0.5" />
|
|
<div>
|
|
<p class="font-medium">Are you sure you want to detach this drive?</p>
|
|
<p class="mt-1 text-warning/80">The data will be preserved but the drive will no longer be accessible to the VM until reattached.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if($status && $status->status !== 'stopped')
|
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-danger/10 border border-danger/30 text-danger text-sm">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
|
<span>VM must be stopped to detach drives.</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeDetachModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="detachDrive"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-warning text-white hover:bg-warning/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@if($status && $status->status !== 'stopped') disabled @endif>
|
|
<x-loading target="detachDrive" />
|
|
<span wire:loading.remove wire:target="detachDrive">Detach Drive</span>
|
|
<span wire:loading wire:target="detachDrive">Detaching...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Delete Drive Confirmation Modal -->
|
|
@if($showDeleteModal && $deletingDriveKey && isset($drives[$deletingDriveKey]))
|
|
@php $deleteDrive = $drives[$deletingDriveKey]; @endphp
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeDeleteModal()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" wire:click="closeDeleteModal"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-danger/10">
|
|
<x-ri-delete-bin-line class="size-5 text-danger" />
|
|
</div>
|
|
<h3 class="text-lg font-semibold">Delete Drive</h3>
|
|
</div>
|
|
<button wire:click="closeDeleteModal" class="text-base/50 hover:text-base transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<div class="p-3 rounded-lg bg-neutral/20 text-sm">
|
|
<p><span class="text-base/50">Drive:</span> {{ $deleteDrive['label'] }} ({{ $deletingDriveKey }})</p>
|
|
<p class="mt-1"><span class="text-base/50">Size:</span> {{ number_format($deleteDrive['size'], 1) }} GB</p>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-2 p-3 rounded-lg bg-danger/10 border border-danger/30 text-danger text-sm">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="font-medium">Are you sure you want to DELETE this drive?</p>
|
|
<p class="mt-1 text-danger/80">This action cannot be undone. All data on this drive will be permanently lost!</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if($status && $status->status !== 'stopped')
|
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-danger/10 border border-danger/30 text-danger text-sm">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
|
<span>VM must be stopped to delete drives.</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeDeleteModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="deleteDrive"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-error text-white hover:bg-error/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@if($status && $status->status !== 'stopped') disabled @endif>
|
|
<x-loading target="deleteDrive" />
|
|
<span wire:loading.remove wire:target="deleteDrive">Delete Drive</span>
|
|
<span wire:loading wire:target="deleteDrive">Deleting...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Reattach Drive Modal -->
|
|
@if($showReattachModal)
|
|
<div class="fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="$wire.closeReattachModal()">
|
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" wire:click="closeReattachModal"></div>
|
|
<div class="flex min-h-full items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg transform overflow-hidden rounded-xl bg-background border border-neutral shadow-xl transition-all">
|
|
<div class="flex items-center justify-between p-5 border-b border-neutral">
|
|
<h3 class="text-lg font-semibold">Reattach Drive</h3>
|
|
<button wire:click="closeReattachModal" class="text-foreground-secondary hover:text-foreground transition-colors" aria-label="Close">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
@if($reattachingDriveKey && isset($unusedDrives[$reattachingDriveKey]))
|
|
<div class="bg-background-secondary rounded-lg p-3 border border-neutral">
|
|
<div class="flex items-center gap-3">
|
|
<div class="p-2 rounded-lg bg-info/10">
|
|
<x-ri-hard-drive-3-line class="size-5 text-info" />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium">{{ $reattachingDriveKey }}</p>
|
|
<p class="text-xs text-foreground-secondary">
|
|
{{ $unusedDrives[$reattachingDriveKey]['size_formatted'] ?? 'Unknown size' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Target Bus Type</label>
|
|
<select wire:model="reattachTargetBus"
|
|
class="w-full px-4 py-2 text-sm rounded-lg border border-neutral bg-background focus:outline-none focus:ring-2 focus:ring-primary/50">
|
|
<option value="">Select bus type...</option>
|
|
@php
|
|
// Calculate next available slot for each bus type
|
|
$busLimits = ['ide' => 4, 'sata' => 6, 'scsi' => 31, 'virtio' => 16];
|
|
$busLabels = ['ide' => 'IDE', 'sata' => 'SATA', 'scsi' => 'SCSI', 'virtio' => 'VirtIO'];
|
|
@endphp
|
|
@foreach($busLimits as $bus => $maxSlots)
|
|
@php
|
|
$nextSlot = null;
|
|
for ($i = 0; $i < $maxSlots; $i++) {
|
|
if (!isset($drives[$bus . $i])) {
|
|
$nextSlot = $i;
|
|
break;
|
|
}
|
|
}
|
|
$isFull = $nextSlot === null;
|
|
@endphp
|
|
<option value="{{ $bus }}" @if($isFull) disabled @endif>
|
|
{{ $busLabels[$bus] }} @if($isFull) (Full) @else (Next: {{ $bus }}{{ $nextSlot }}) @endif
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-2 p-3 rounded-lg bg-info/10 border border-info/30 text-info text-sm">
|
|
<x-ri-information-line class="size-4 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p>The drive will be reattached to the lowest available slot on the selected bus.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if($status && $status->status !== 'stopped')
|
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-danger/10 border border-danger/30 text-danger text-sm">
|
|
<x-ri-error-warning-line class="size-4 flex-shrink-0" />
|
|
<span>VM must be stopped to reattach drives.</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 p-5 border-t border-neutral">
|
|
<button wire:click="closeReattachModal"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg border border-neutral bg-background hover:bg-background-secondary transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="reattachDrive"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
|
@if(!$reattachTargetBus || ($status && $status->status !== 'stopped')) disabled @endif>
|
|
<x-loading target="reattachDrive" />
|
|
<span wire:loading.remove wire:target="reattachDrive">Reattach Drive</span>
|
|
<span wire:loading wire:target="reattachDrive">Reattaching...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<script data-navigate-track>
|
|
// Use event delegation to avoid duplicate listeners
|
|
if (!window.__storageRefreshListenerAdded) {
|
|
window.__storageRefreshListenerAdded = true;
|
|
document.addEventListener('livewire:initialized', () => {
|
|
Livewire.on('refresh-page', () => {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 500);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
</x-proxmox::layout>
|