767 lines
54 KiB
PHP
767 lines
54 KiB
PHP
<x-proxmox::layout :server="$server" view="boot-order" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
<div class="space-y-4 lg:space-y-6 px-2 sm:px-0 overflow-x-hidden">
|
|
<div class="flex items-center gap-1.5 text-xs text-base/50 mb-2">
|
|
<a href="{{ route('proxmox.server.overview', ['server' => $server->id]) }}" wire:navigate class="hover:text-primary transition-colors">Overview</a>
|
|
<x-ri-arrow-right-s-line class="size-3" />
|
|
<span class="text-base/70">Boot Order</span>
|
|
</div>
|
|
<div class="bg-info/10 border border-info/30 rounded-lg p-3 lg:p-4"
|
|
x-data="{
|
|
isCollapsed: localStorage.getItem('boot-order-notice-collapsed') === 'true',
|
|
toggle() {
|
|
this.isCollapsed = !this.isCollapsed;
|
|
localStorage.setItem('boot-order-notice-collapsed', this.isCollapsed);
|
|
}
|
|
}">
|
|
<div class="flex items-start gap-3">
|
|
<x-ri-information-line class="size-5 text-info flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="text-sm font-semibold text-base">Cloud-init Configuration Notice</h3>
|
|
<button @click="toggle()" type="button" class="text-base/60 hover:text-base transition-colors">
|
|
<x-ri-arrow-down-s-line x-bind:class="isCollapsed ? 'size-4 transition-transform rotate-180' : 'size-4 transition-transform'" />
|
|
</button>
|
|
</div>
|
|
<div x-show="!isCollapsed" x-transition>
|
|
<p class="text-xs text-base/70 leading-relaxed">
|
|
<strong>Hostname, password, and networking settings</strong> are configured automatically via <strong>cloud-init</strong>.
|
|
This applies to standard operating system installations that support cloud-init. However, if you are:
|
|
</p>
|
|
<ul class="text-xs text-base/70 leading-relaxed mt-2 ml-4 list-disc space-y-1">
|
|
<li>Installing from an ISO image</li>
|
|
<li>Configuring the operating system manually during installation</li>
|
|
<li>Using Windows (which does not support cloud-init)</li>
|
|
</ul>
|
|
<p class="text-xs text-base/70 leading-relaxed mt-2">
|
|
You will need to <strong>manually configure networking, security settings, and hostname</strong> within the operating system, as cloud-init will not be available in these scenarios.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<h2 class="text-lg sm:text-xl font-semibold break-words">Boot Order</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-settings-3-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Boot Order Configuration
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Determines the sequence in which the virtual machine attempts to boot from available devices. The system will try each device in the specified order until it finds a bootable medium.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Note:</span> If no explicit boot order is configured, QEMU uses the default platform boot sequence. Changes take effect on the next power event (start, reboot, or reset). The boot order is populated directly from the QEMU configuration via the QEMU API.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-base/60">
|
|
Control the sequence used when attempting to boot this virtual machine. The list below is
|
|
populated directly from the QEMU configuration via the API.
|
|
</p>
|
|
<p class="text-xs text-base/50 break-words overflow-wrap-anywhere">
|
|
Currently applied:
|
|
@if (count($currentOrder))
|
|
<span class="inline-block">{{ implode(' → ', array_map(fn ($device) => $availableDevices[$device]['label'] ?? strtoupper($device), $currentOrder)) }}</span>
|
|
@else
|
|
Default platform order (no explicit devices configured)
|
|
@endif
|
|
</p>
|
|
</div>
|
|
|
|
@if(count($cdDrives) < 2)
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-3 lg:p-4 space-y-3 lg:space-y-4">
|
|
<div class="flex flex-wrap items-center justify-between gap-2 min-w-0">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
<h3 class="text-base sm:text-lg font-semibold break-words">Create CD/DVD-ROM Drive</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-dvd-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Create CD/DVD-ROM Drive
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Adds a virtual optical drive to your VM using the next available IDE or SATA slot. The system automatically selects an unused slot (IDE0-IDE4 or SATA0-SATA5).
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75 mb-2">
|
|
<span class="font-semibold text-base/90">Limitations:</span> Cloud-init media devices are automatically ignored. Maximum of 2 CD/DVD-ROM drives per virtual machine.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
Once created, the drive will appear in the boot order and can be used to mount ISO images for operating system installation or other purposes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/60">
|
|
Use the next available IDE or SATA slot to add a virtual optical drive. Cloud-init media is ignored. Maximum of 2 drives allowed.
|
|
</p>
|
|
</div>
|
|
<span class="text-xs font-semibold {{ count($cdDrives) === 0 ? 'text-warning' : 'text-base/60' }}">
|
|
{{ count($cdDrives) === 0 ? 'No drive configured' : count($cdDrives) . ' of 2 drives' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<x-button.primary wire:click="createCdDrive" wire:loading.attr="disabled"
|
|
class="w-full sm:w-auto"
|
|
:disabled="count($cdDrives) >= 2">
|
|
<x-loading target="createCdDrive" />
|
|
Create CD/DVD-ROM Drive
|
|
</x-button.primary>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@foreach($cdDrives as $index => $drive)
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-3 lg:p-4 space-y-3 lg:space-y-4" wire:key="cd-drive-{{ $drive }}">
|
|
<div class="flex flex-wrap items-center justify-between gap-2 min-w-0">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<h3 class="text-base sm:text-lg font-semibold break-words">Mount ISO Image - {{ strtoupper($drive) }}</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-file-upload-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Mount ISO Image - {{ strtoupper($drive) }}
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Attach or detach ISO files from cephfs storage to the virtual CD/DVD-ROM drive. Select an ISO from the dropdown to mount it, or choose "None" to leave the drive empty.
|
|
</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">Apply:</span> Commits your selection and immediately mounts the ISO to the drive.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Eject:</span> Quickly removes the currently mounted ISO, setting the drive to empty.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
ISO images are stored in cephfs storage and can be used for operating system installation, recovery, or running live environments. The storage prefix is hidden in the dropdown for clarity.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/60">
|
|
Attach or detach an ISO from cephfs storage for the drive on {{ strtoupper($drive) }}.
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" wire:click="removeCdDrive('{{ $drive }}')"
|
|
wire:loading.attr="disabled"
|
|
class="text-xs font-semibold text-error hover:text-error/80">
|
|
<x-loading target="removeCdDrive('{{ $drive }}')" />
|
|
<span wire:loading.remove wire:target="removeCdDrive('{{ $drive }}')">Remove</span>
|
|
<span wire:loading wire:target="removeCdDrive('{{ $drive }}')">Removing...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center">
|
|
<div class="relative flex-1 min-w-0">
|
|
<select id="iso-selection-{{ $drive }}"
|
|
wire:model.live="selectedIsos.{{ $drive }}"
|
|
class="w-full bg-background border border-neutral rounded-md pl-3 pr-10 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary appearance-none">
|
|
@foreach($isoOptions as $option)
|
|
<option value="{{ $option['value'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
<!-- Dropdown Arrow Icon -->
|
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
<svg class="w-4 h-4 text-base/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 w-full sm:w-auto min-w-0">
|
|
<x-button.primary wire:click="applyIso('{{ $drive }}')"
|
|
wire:loading.attr="disabled"
|
|
class="flex-1 sm:flex-initial"
|
|
:disabled="($currentIsos[$drive] ?? 'none') === ($selectedIsos[$drive] ?? 'none')">
|
|
<x-loading target="applyIso('{{ $drive }}')" />
|
|
<span wire:loading.remove wire:target="applyIso('{{ $drive }}')">Apply</span>
|
|
<span wire:loading wire:target="applyIso('{{ $drive }}')">Applying...</span>
|
|
</x-button.primary>
|
|
<x-button.danger wire:click="ejectIso('{{ $drive }}')"
|
|
wire:loading.attr="disabled"
|
|
class="flex-1 sm:flex-initial"
|
|
:disabled="!isset($currentIsos[$drive]) || $currentIsos[$drive] === 'none'">
|
|
<x-loading target="ejectIso('{{ $drive }}')" />
|
|
<span wire:loading.remove wire:target="ejectIso('{{ $drive }}')">Eject</span>
|
|
<span wire:loading wire:target="ejectIso('{{ $drive }}')">Ejecting...</span>
|
|
</x-button.danger>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/50">
|
|
Select "None" to leave the drive empty. Storage prefix is hidden for clarity.
|
|
</p>
|
|
|
|
@if(isset($currentIsos[$drive]) && $currentIsos[$drive] !== 'none')
|
|
<div class="text-xs text-base/60">
|
|
Currently mounted: <strong>{{ $this->formatIsoLabel($currentIsos[$drive]) }}</strong>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
|
|
@if(count($cdDrives) > 0)
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-3 lg:p-4 space-y-3 lg:space-y-4">
|
|
<div class="flex flex-wrap items-center justify-between gap-2 min-w-0">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
<h3 class="text-base sm:text-lg font-semibold break-words">Request ISO</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-customer-service-2-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Request ISO Image
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Opens a support ticket form where you can request that a specific ISO image be added to the cephfs storage.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-semibold leading-relaxed text-base/90 mb-2.5">
|
|
When submitting a request, include:
|
|
</p>
|
|
<ul class="block font-sans text-xs font-normal leading-relaxed text-base/75 space-y-1.5 ml-4 list-disc mb-3">
|
|
<li>An active public URL of the image in <span class="font-semibold text-base/90">.iso format</span></li>
|
|
<li>A project page of the distribution</li>
|
|
<li>Name and version of the distribution</li>
|
|
</ul>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
Submit your request under the <span class="font-semibold text-base/90">"ISO Request"</span> department for faster processing. Once added, the ISO will appear in the ISO selection dropdown for all users.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/60">
|
|
Submit a support ticket to request an ISO image be added to cephfs storage.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<x-button.primary
|
|
onclick="window.open('https://billing.virtnet.bond/tickets/create', '_blank')"
|
|
class="w-full sm:w-auto">
|
|
Request ISO
|
|
</x-button.primary>
|
|
|
|
<div class="bg-info/10 border border-info/30 rounded-lg p-2 sm:p-3">
|
|
<div class="flex items-start gap-2">
|
|
<x-ri-information-line class="size-4 text-info flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<p class="text-xs text-base/70 leading-relaxed">
|
|
When requesting an ISO, please ensure your ticket includes:
|
|
</p>
|
|
<ul class="text-xs text-base/70 leading-relaxed mt-1.5 ml-4 list-disc space-y-1">
|
|
<li>An active public URL of the image in <strong>.iso format</strong></li>
|
|
<li>A project page of the distribution</li>
|
|
<li>Name and version of the distribution</li>
|
|
</ul>
|
|
<p class="text-xs text-base/70 leading-relaxed mt-1.5">
|
|
Please submit your request under the <strong>"ISO Request"</strong> department.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6">
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
<h3 class="text-base sm:text-lg font-semibold break-words">Boot priority</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-list-ordered class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Boot Priority
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Shows the current boot sequence for your virtual machine. Devices are listed in order from highest to lowest priority. The VM will attempt to boot from each device in sequence until it finds a bootable medium.
|
|
</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">Move up/down:</span> Reorder devices to adjust boot priority.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Remove:</span> Exclude a device from the boot sequence.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
The "Synced" indicator shows when your changes match the saved configuration; "Unsaved changes" indicates modifications that need to be saved. Changes take effect on the next power event.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/60">Move devices up or down to adjust the order.</p>
|
|
</div>
|
|
<span class="text-xs font-medium {{ $this->hasChanges ? 'text-warning' : 'text-success' }}">
|
|
{{ $this->hasChanges ? 'Unsaved changes' : 'Synced' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
@forelse ($order as $index => $device)
|
|
<div wire:key="boot-order-{{ $device }}"
|
|
class="flex flex-col gap-2 border border-neutral/60 rounded-md bg-background px-2 sm:px-3 py-2 overflow-hidden">
|
|
<div class="flex items-start justify-between gap-2 sm:gap-3 min-w-0">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-semibold break-words">
|
|
{{ $index + 1 }}. {{ $availableDevices[$device]['label'] ?? strtoupper($device) }}
|
|
</p>
|
|
@if ($availableDevices[$device]['details'] ?? false)
|
|
<p class="text-xs text-base/60 break-words mt-1">
|
|
{{ $availableDevices[$device]['details'] }}
|
|
</p>
|
|
@endif
|
|
</div>
|
|
<button wire:click="removeDevice('{{ $device }}')" type="button"
|
|
class="text-xs text-error hover:text-error/80 font-semibold">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex flex-row gap-2">
|
|
<button wire:click="moveDevice('{{ $device }}', 'up')" type="button"
|
|
class="flex-1 border border-neutral rounded-md py-1 text-xs font-semibold {{ $index === 0 ? 'text-base/30 cursor-not-allowed opacity-50' : 'hover:bg-background-secondary' }}"
|
|
@disabled($index === 0)>
|
|
↑ Move up
|
|
</button>
|
|
<button wire:click="moveDevice('{{ $device }}', 'down')" type="button"
|
|
class="flex-1 border border-neutral rounded-md py-1 text-xs font-semibold {{ $index === count($order) - 1 ? 'text-base/30 cursor-not-allowed opacity-50' : 'hover:bg-background-secondary' }}"
|
|
@disabled($index === count($order) - 1)>
|
|
↓ Move down
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="border border-dashed border-neutral rounded-md px-4 py-6 text-center text-sm text-base/60">
|
|
Select devices from the list on the right to build a boot order.
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
|
|
@php
|
|
$remainingDevices = array_values(array_diff(array_keys($availableDevices), $order));
|
|
@endphp
|
|
|
|
<div class="bg-background-secondary border border-neutral rounded-lg p-4 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
<h3 class="text-base sm:text-lg font-semibold break-words">Available devices</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-hard-drive-2-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Available Devices
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Lists all bootable devices detected in your VM configuration that are not currently part of the boot sequence.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75 mb-2">
|
|
<span class="font-semibold text-base/90">Device Types:</span> Disk drives (VirtIO, SCSI, SATA, IDE, NVMe), network adapters, and CD/DVD-ROM drives. Each device shows its type, identifier, and configuration details.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
Click <span class="font-semibold text-base/90">Add</span> to include a device in the boot priority list. Devices are automatically detected from the QEMU configuration via the QEMU API.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-base/60">These devices can be part of the boot sequence.</p>
|
|
</div>
|
|
<span class="text-xs text-base/60">{{ count($remainingDevices) }} available</span>
|
|
</div>
|
|
|
|
<div class="space-y-2 max-h-[32rem] overflow-y-auto pr-1 -mx-1 px-1">
|
|
@forelse ($remainingDevices as $device)
|
|
<div wire:key="boot-device-{{ $device }}"
|
|
class="flex flex-col gap-2 border border-neutral/60 rounded-md bg-background px-2 sm:px-3 py-2 overflow-hidden">
|
|
<div class="flex items-start justify-between gap-2 sm:gap-3 min-w-0">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-semibold break-words">{{ $availableDevices[$device]['label'] ?? strtoupper($device) }}</p>
|
|
@if ($availableDevices[$device]['details'] ?? false)
|
|
<p class="text-xs text-base/60 break-words mt-1">{{ $availableDevices[$device]['details'] }}</p>
|
|
@endif
|
|
</div>
|
|
<button wire:click="addDevice('{{ $device }}')" type="button"
|
|
class="text-xs font-semibold text-primary hover:text-primary/80">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="border border-dashed border-neutral rounded-md px-4 py-6 text-center text-sm text-base/60">
|
|
All detected devices are already part of the boot priority list.
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
|
<div class="flex-1 w-full sm:w-auto">
|
|
<x-button.secondary wire:click="refreshBootConfiguration" wire:loading.attr="disabled"
|
|
class="w-full">
|
|
<x-loading target="refreshBootConfiguration" />
|
|
Refresh
|
|
</x-button.secondary>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-1 w-full sm:w-auto">
|
|
<x-button.secondary wire:click="resetOrder" :disabled="!$this->hasChanges"
|
|
class="flex-1">
|
|
Reset to current
|
|
</x-button.secondary>
|
|
<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" @disabled(!$this->hasChanges)>
|
|
<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-restart-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Reset to Current
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Discards any unsaved changes to the boot order and restores it to match the last saved configuration.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
This button is only enabled when there are unsaved changes. Use this if you want to undo modifications without refreshing the entire configuration from the server.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-1 w-full sm:w-auto">
|
|
<x-button.primary wire:click="saveOrder" wire:loading.attr="disabled" :disabled="!$this->hasChanges"
|
|
class="flex-1">
|
|
<x-loading target="saveOrder" />
|
|
Save boot order
|
|
</x-button.primary>
|
|
<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" @disabled(!$this->hasChanges)>
|
|
<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-save-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight text-base">
|
|
Save Boot Order
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Commits your boot order changes to the QEMU server configuration. The new boot sequence will be applied to the virtual machine and will take effect on the next power event (start, reboot, or reset).
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75 mb-2">
|
|
If the boot order is empty, the system will reset to the default platform boot sequence.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
This button is only enabled when there are unsaved changes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</x-proxmox::layout>
|
|
|