refactor: streamline UI elements and improve layout for better readability and consistency

This commit is contained in:
2025-12-17 13:59:23 -05:00
parent 35d504072d
commit 7794570770

View File

@@ -314,19 +314,14 @@
<div class="pt-3 border-t border-gray-200 dark:border-gray-700 space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<p><span class="font-semibold text-gray-700 dark:text-gray-300">CPU:</span> Live usage % of vCPU entitlement.</p>
<p><span class="font-semibold text-gray-700 dark:text-gray-300">Memory:</span> RAM usage vs provisioned.</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Hypervisor Uptime:</span> Shows the live uptime plus power event statistics from the task history.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Status Badges:</span> "Healthy" (green) for normal usage, "Watchlist" (yellow) at 85%+, "Over entitlement" (red) when exceeded.
</p>
<p><span class="font-semibold text-gray-700 dark:text-gray-300">Uptime:</span> Live uptime + power events.</p>
<p><span class="font-semibold text-gray-700 dark:text-gray-300">Badges:</span> Healthy (green), Watchlist (yellow 85%+), Over (red).</p>
</div>
</x-proxmox::ui.help-modal>
<p class="text-sm text-base/70 ml-2 hidden md:block">Instantly compare entitlement with current consumption.</p>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2 hidden md:inline">Live refresh</span>
</div>
<p class="text-xs text-base/50">Values refresh alongside the live status poll.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
@foreach($resourceAllocations as $resource)
<x-proxmox::ui.allocation-card
:label="$resource['label']"
@@ -341,114 +336,96 @@
</div>
</x-proxmox::ui.card>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mt-8 md:items-stretch">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4 md:items-stretch">
<div class="grid gap-8 items-start">
<div class="grid gap-4 items-start">
<!-- Details -->
<div class="">
<div>
<div>
<div style="background-image: url('https://billing.virtnet.bond/assets/images/main.png'); background-size: 100%; height: 5.5vh;" class="flex flex-col p-4 rounded-t-lg border-t border-x border-neutral"></div>
<div style="background-image: url('https://billing.virtnet.bond/assets/images/main.png'); background-size: cover; height: 3rem;" class="rounded-t-lg border-t border-x border-gray-200 dark:border-gray-700"></div>
<!-- Status Section -->
<div class="bg-background-secondary border-x border-neutral p-4 space-y-4 rounded-m-lg">
<div class="bg-white dark:bg-gray-800 border-x border-gray-200 dark:border-gray-700 p-3 space-y-3">
<!-- Status -->
<div class="space-y-2">
<label class="text-sm font-semibold text-base/80">Status</label>
<div class="flex items-center gap-2 px-3 py-2 text-sm text-base/80">
<div class="relative inline-flex size-5">
<div class="flex items-center justify-between">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">Status</label>
<div class="flex items-center gap-1.5 text-sm">
<div class="relative inline-flex size-4">
@if ($status && $status->status === 'running')
<span class="absolute inset-0 rounded-full bg-success/20 animate-ping"></span>
<span class="relative flex items-center justify-center size-5 p-1 rounded-full text-success bg-success/20">
<x-ri-circle-fill />
<span class="absolute inset-0 rounded-full bg-green-500 animate-ping opacity-75"></span>
<span class="relative flex items-center justify-center size-4 rounded-full text-green-600 bg-green-100 dark:bg-green-900">
<x-ri-circle-fill class="size-2" />
</span>
@else
<span class="flex items-center justify-center size-5 p-1 rounded-full text-warning bg-warning/20">
<x-ri-error-warning-fill />
<span class="flex items-center justify-center size-4 rounded-full text-amber-600 bg-amber-100 dark:bg-amber-900">
<x-ri-error-warning-fill class="size-2" />
</span>
@endif
</div>
<span class="truncate flex-1">
<span class="font-medium text-gray-800 dark:text-gray-200">
{{ $status && $status->status === 'running' ? 'Running' : 'Stopped' }}
</span>
</div>
</div>
<!-- Operating System -->
<div class="space-y-2">
<label class="text-sm font-semibold text-base/80">Operating System</label>
<div class="flex items-center gap-2 px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
{{ $server->os->name }}
</span>
</div>
<div class="flex items-center justify-between">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">OS</label>
<span class="text-sm text-gray-800 dark:text-gray-200">{{ $server->os->name }}</span>
</div>
<!-- Hostname -->
<div class="space-y-2" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
<label class="text-sm font-semibold text-base/80">Hostname</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
{{ $server->hostname ?? $status->name ?? 'N/A' }}
</span>
<button wire:click="startEditingHostname" type="button"
class="text-base/70 hover:text-base/60 ml-2"
aria-label="Edit Hostname">
<x-ri-edit-fill class="inline size-4" />
</button>
</div>
<div class="flex items-center justify-between" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">Hostname</label>
<div class="flex items-center gap-2">
<span class="text-sm font-mono text-gray-800 dark:text-gray-200">{{ $server->hostname ?? $status->name ?? 'N/A' }}</span>
<button wire:click="startEditingHostname" type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400">
<x-ri-edit-fill class="size-3.5" />
</button>
</div>
<!-- Hostname Edit Modal -->
<!-- Modal Backdrop -->
<div x-cloak x-show="showHostnameEditModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showHostnameEditModal = false; $wire.cancelEditingHostname()"
@keydown.escape.window="showHostnameEditModal = false; $wire.cancelEditingHostname()"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-50 bg-gray-900 bg-opacity-50"
style="display: none;">
</div>
<!-- Modal -->
<div x-cloak x-show="showHostnameEditModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
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="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="showHostnameEditModal = false; $wire.cancelEditingHostname()"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4">
<div class="relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl w-full max-w-md p-4 space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit Hostname</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showHostnameEditModal = false; $wire.cancelEditingHostname()"
aria-label="Close hostname edit modal">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Edit Hostname</h4>
<button type="button" class="text-gray-400 hover:text-gray-600 p-1"
@click="showHostnameEditModal = false; $wire.cancelEditingHostname()">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-3">
<div class="space-y-2">
<div>
<label class="text-xs font-semibold text-base/70">Hostname</label>
<input type="text"
wire:model.defer="hostname"
class="form-input w-full mt-1"
placeholder="Enter hostname" />
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">Hostname</label>
<input type="text" wire:model.defer="hostname" class="form-input w-full mt-1 text-sm" placeholder="Enter hostname" />
</div>
@if($hostnameError)
<div class="text-sm text-error">{{ $hostnameError }}</div>
<div class="text-xs text-red-600">{{ $hostnameError }}</div>
@endif
<p class="text-xs text-base/60">
<strong>Save:</strong> Updates the hostname in the database and order properties.<br>
<strong>Save & Apply:</strong> Saves to database and applies the hostname. (Will be configured on next boot via cloud-init)
<p class="text-xs text-gray-500 dark:text-gray-400">
<strong>Save:</strong> Database only. <strong>Save & Apply:</strong> + cloud-init on next boot.
</p>
</div>
<div class="flex justify-end gap-2">
@@ -468,119 +445,71 @@
<!-- Primary IPv4 Address -->
@if($server->primaryIpv4)
<div class="space-y-2">
<label class="text-sm font-semibold text-base/80">Primary IPv4 Address</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80"
x-data="{
copied: false,
copyText: '{{ $server->primaryIpv4->ip }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}">
<span class="truncate flex-1">
{{ $server->primaryIpv4->ip }}
</span>
<button @click="copyToClipboard()" type="button"
class="text-base/70 hover:text-base/60 ml-2 relative inline-flex items-center justify-center w-4 h-4"
aria-label="Copy IPv4 Address">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
<div class="flex items-center justify-between" x-data="{ copied: false, copyText: '{{ $server->primaryIpv4->ip }}', copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); } }">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">IPv4</label>
<div class="flex items-center gap-1.5">
<span class="text-sm font-mono text-gray-800 dark:text-gray-200">{{ $server->primaryIpv4->ip }}</span>
<button @click="copy()" type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400">
<x-ri-check-fill x-show="copied" class="size-3.5 text-green-600" />
<x-ri-file-copy-fill x-show="!copied" class="size-3.5" />
</button>
</div>
</div>
@endif
<!-- Primary IPv6 Address -->
@if($server->primaryIpv6)
<div class="space-y-2">
<label class="text-sm font-semibold text-base/80">Primary IPv6 Address</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80"
x-data="{
copied: false,
copyText: '{{ $server->primaryIpv6->ip }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}">
<span class="truncate flex-1">
{{ $server->primaryIpv6->ip }}
</span>
<button @click="copyToClipboard()" type="button"
class="text-base/70 hover:text-base/60 ml-2 relative inline-flex items-center justify-center w-4 h-4"
aria-label="Copy IPv6 Address">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
<div class="flex items-center justify-between" x-data="{ copied: false, copyText: '{{ $server->primaryIpv6->ip }}', copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); } }">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">IPv6</label>
<div class="flex items-center gap-1.5">
<span class="text-xs font-mono text-gray-800 dark:text-gray-200 truncate max-w-[180px]">{{ $server->primaryIpv6->ip }}</span>
<button @click="copy()" type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400">
<x-ri-check-fill x-show="copied" class="size-3.5 text-green-600" />
<x-ri-file-copy-fill x-show="!copied" class="size-3.5" />
</button>
</div>
</div>
@endif
<!-- Root Password -->
<div class="space-y-2" x-data="{ showPasswordViewModal: false, showPasswordEditModal: @entangle('isEditingPassword') }">
<label class="text-sm font-semibold text-base/80">Root Password</label>
<div class="space-y-2">
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80">
<span class="truncate flex-1">
@if(isset($server->password) && $server->password)
••••••••••••
@else
Password is sent via email on install.
@endif
</span>
<div class="flex items-center justify-between" x-data="{ showPasswordViewModal: false, showPasswordEditModal: @entangle('isEditingPassword') }">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400">Password</label>
<div class="flex items-center gap-1.5">
<span class="text-sm text-gray-800 dark:text-gray-200">
@if(isset($server->password) && $server->password)
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
@click="showPasswordViewModal = true"
aria-label="View Root Password">
<x-ri-eye-line class="inline size-4" />
</button>
••••••••
@else
Emailed
@endif
<button type="button"
class="text-base/70 hover:text-base/60 ml-2"
wire:click="startEditingPassword"
aria-label="Edit Root Password">
<x-ri-edit-fill class="inline size-4" />
</span>
@if(isset($server->password) && $server->password)
<button type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400" @click="showPasswordViewModal = true">
<x-ri-eye-line class="size-3.5" />
</button>
</div>
@endif
<button type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400" wire:click="startEditingPassword">
<x-ri-edit-fill class="size-3.5" />
</button>
</div>
<!-- Password View Modal -->
<!-- Modal Backdrop -->
<div x-cloak x-show="showPasswordViewModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showPasswordViewModal = false"
@keydown.escape.window="showPasswordViewModal = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-50 bg-gray-900 bg-opacity-50"
style="display: none;">
</div>
<!-- Modal -->
<div x-cloak x-show="showPasswordViewModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
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="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="showPasswordViewModal = false"
@@ -589,29 +518,20 @@
x-data="{
copied: false,
copyText: {{ json_encode($server->password) }},
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
}">
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4">
<div class="relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl w-full max-w-md p-4 space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Root Password</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showPasswordViewModal = false"
aria-label="Close password view modal">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Root Password</h4>
<button type="button" class="text-gray-400 hover:text-gray-600 p-1" @click="showPasswordViewModal = false">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="bg-background border border-neutral rounded-md p-3 text-sm text-base/80 overflow-auto">
<code class="whitespace-pre-wrap break-all block">{{ $server->password }}</code>
<div class="bg-gray-100 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded p-2 text-sm text-gray-800 dark:text-gray-200 overflow-auto">
<code class="break-all block font-mono text-xs">{{ $server->password }}</code>
</div>
<div class="flex justify-end">
<button type="button"
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-white text-xs font-semibold hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
@click="copyToClipboard()">
<button type="button" class="px-3 py-1.5 rounded bg-blue-600 text-white text-xs font-medium hover:bg-blue-700" @click="copy()">
<span x-show="!copied">Copy</span>
<span x-show="copied">Copied!</span>
</button>
@@ -620,61 +540,53 @@
</div>
<!-- Password Edit Modal -->
<!-- Modal Backdrop -->
<div x-cloak x-show="showPasswordEditModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()"
@keydown.escape.window="showPasswordEditModal = false; $wire.cancelEditingPassword()"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-50 bg-gray-900 bg-opacity-50"
style="display: none;">
</div>
<!-- Modal -->
<div x-cloak x-show="showPasswordEditModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter="transition ease-out duration-200"
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="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="showPasswordEditModal = false; $wire.cancelEditingPassword()"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative bg-background-secondary border border-neutral rounded-lg shadow-xl w-full max-w-xl mx-4 p-5 space-y-4">
<div class="relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl w-full max-w-md p-4 space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-base font-semibold text-base/90">Edit Root Password</h4>
<button type="button"
class="bg-background border border-neutral p-1.5 rounded-md hover:bg-background/80"
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()"
aria-label="Close password edit modal">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-100">Edit Root Password</h4>
<button type="button" class="text-gray-400 hover:text-gray-600 p-1"
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()">
<x-ri-close-line class="size-4" />
</button>
</div>
<div class="space-y-3">
<div class="space-y-2">
<div>
<label class="text-xs font-semibold text-base/70">New Password</label>
<input type="password"
wire:model.defer="password"
class="form-input w-full mt-1"
placeholder="Enter new password" />
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">New Password</label>
<input type="password" wire:model.defer="password" class="form-input w-full mt-1 text-sm" placeholder="Enter new password" />
</div>
<div>
<label class="text-xs font-semibold text-base/70">Confirm Password</label>
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">Confirm Password</label>
<input type="password"
wire:model.defer="password_confirmation"
class="form-input w-full mt-1"
class="form-input w-full mt-1 text-sm"
placeholder="Confirm password" />
</div>
@if($passwordError)
<div class="text-sm text-error">{{ $passwordError }}</div>
<div class="text-xs text-red-600">{{ $passwordError }}</div>
@endif
<p class="text-xs text-base/60">
<strong>Save:</strong> Updates the password in the database.<br>
<strong>Save & Apply:</strong> Saves to database and applies the password to the VPS via cloud-init. <strong>The server must be rebooted for the password change to take effect.</strong>
<p class="text-xs text-gray-500 dark:text-gray-400">
<strong>Save:</strong> Database only. <strong>Save & Apply:</strong> + cloud-init (reboot required).
</p>
</div>
<div class="flex justify-end gap-2">
@@ -694,16 +606,12 @@
</div>
</div>
<div class="flex flex-col bg-error/10 border border-error/30 p-4 rounded-b-lg">
<label class="text-lg font-semibold flex flex-row justify-between items-center gap-8" for="os">
<div class="flex flex-col gap-2">
Reinstall Server
<p class="text-sm font-semibold text-base/60">The data on the server will be permanently
deleted. This action is irreversible and cannot be undone.</p>
</div>
<livewire:proxmox.reinstall :server="$server" />
<div class="flex items-center justify-between bg-red-50 dark:bg-red-950 border-t border-x border-red-200 dark:border-red-800 px-3 py-2 rounded-b-lg">
<div>
<span class="text-sm font-semibold text-red-700 dark:text-red-300">Reinstall Server</span>
<p class="text-xs text-red-600 dark:text-red-400">Data permanently deleted. Irreversible.</p>
</div>
<livewire:proxmox.reinstall :server="$server" />
</label>
</div>