Enhance Proxmox views with improved loading states and UI updates
- 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.
This commit is contained in:
@@ -53,9 +53,26 @@
|
||||
<x-ri-pencil-fill class="size-2.5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-2xl font-bold">
|
||||
{{ $server->hostname }}
|
||||
</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl font-bold">
|
||||
{{ $server->hostname }}
|
||||
</span>
|
||||
@if($vmStatus === 'running')
|
||||
<span class="relative flex h-2.5 w-2.5" title="Server is running">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-success"></span>
|
||||
</span>
|
||||
@elseif($vmStatus === 'stopped')
|
||||
<span class="relative flex h-2.5 w-2.5" title="Server is stopped">
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-warning"></span>
|
||||
</span>
|
||||
@elseif($vmStatus)
|
||||
<span class="relative flex h-2.5 w-2.5" title="Status: {{ $vmStatus }}">
|
||||
<span class="animate-pulse absolute inline-flex h-full w-full rounded-full bg-info opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-info"></span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
@@ -1214,12 +1231,20 @@
|
||||
},
|
||||
body: JSON.stringify({ icon: this.icon, color: this.color, prefix: this.iconPrefix })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.message = 'Icon saved!';
|
||||
setTimeout(() => { this.closeModal(); location.reload(); }, 800);
|
||||
} else {
|
||||
this.message = 'Failed to save icon.';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.message = 'Network error. Please try again.';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1453,7 +1478,8 @@
|
||||
<div class="hidden lg:flex flex-row gap-2 ml-auto justify-end items-center">
|
||||
<x-button.primary wire:click="do('start')" class="disabled:cursor-not-allowed"
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'stopped'">
|
||||
:disabled="$vmStatus !== 'stopped'"
|
||||
title="Start the virtual machine">
|
||||
<x-loading target="do('start')" />
|
||||
<x-ri-play-fill class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('start')">
|
||||
@@ -1463,7 +1489,8 @@
|
||||
<div x-show="isAdvancedView" x-transition>
|
||||
<x-button.secondary wire:click="do('shutdown')" class="text-nowrap disabled:cursor-not-allowed"
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Graceful shutdown via ACPI signal">
|
||||
<x-loading target="do('shutdown')" />
|
||||
<x-ri-stop-fill class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('shutdown')">
|
||||
@@ -1473,7 +1500,9 @@
|
||||
</div>
|
||||
<div x-show="isAdvancedView" x-transition>
|
||||
<x-button.secondary wire:click="do('reboot')" class="text-nowrap disabled:cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Graceful reboot via ACPI signal">
|
||||
<x-loading target="do('reboot')" />
|
||||
<x-ri-loop-left-line class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('reboot')">
|
||||
@@ -1482,7 +1511,9 @@
|
||||
</x-button.secondary>
|
||||
</div>
|
||||
<x-button.danger wire:click="do('stop')" class="text-nowrap disabled:cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Force power off — unsaved data may be lost">
|
||||
<x-loading target="do('stop')" />
|
||||
<x-ri-shut-down-line class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('stop')">
|
||||
@@ -1495,7 +1526,8 @@
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<x-button.primary wire:click="do('start')" class="w-full disabled:cursor-not-allowed"
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'stopped'">
|
||||
:disabled="$vmStatus !== 'stopped'"
|
||||
title="Start the virtual machine">
|
||||
<x-loading target="do('start')" />
|
||||
<x-ri-play-fill class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('start')">
|
||||
@@ -1505,7 +1537,8 @@
|
||||
<div x-show="isAdvancedView" x-transition>
|
||||
<x-button.secondary wire:click="do('shutdown')" class="w-full text-nowrap disabled:cursor-not-allowed"
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Graceful shutdown via ACPI signal">
|
||||
<x-loading target="do('shutdown')" />
|
||||
<x-ri-stop-fill class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('shutdown')">
|
||||
@@ -1515,7 +1548,9 @@
|
||||
</div>
|
||||
<div x-show="isAdvancedView" x-transition>
|
||||
<x-button.secondary wire:click="do('reboot')" class="w-full text-nowrap disabled:cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Graceful reboot via ACPI signal">
|
||||
<x-loading target="do('reboot')" />
|
||||
<x-ri-loop-left-line class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('reboot')">
|
||||
@@ -1525,7 +1560,9 @@
|
||||
</div>
|
||||
<x-button.danger wire:click="do('stop')"
|
||||
class="text-nowrap w-full disabled:cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'">
|
||||
wire:loading.attr="disabled" wire:loading.class="opacity-50 cursor-not-allowed"
|
||||
:disabled="$vmStatus !== 'running'"
|
||||
title="Force power off — unsaved data may be lost">
|
||||
<x-loading target="do('stop')" />
|
||||
<x-ri-shut-down-line class="size-4" />
|
||||
<div wire:loading.remove wire:target="do('stop')">
|
||||
@@ -1948,6 +1985,52 @@
|
||||
background: color-mix(in srgb, var(--color-neutral) 30%, transparent);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Hide sidebar in problematic resolution range (768px - 1023px) */
|
||||
@media screen and (min-width: 768px) and (max-width: 1023px) {
|
||||
html body > div.w-full.flex.flex-grow > aside.w-64.h-screen,
|
||||
html body > div.w-full.flex > aside.w-64.h-screen,
|
||||
html body aside.w-64.h-screen.mt-14,
|
||||
html body aside.w-64.h-screen.mt-24,
|
||||
body aside.w-64[class*="md:flex"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
width: 0 !important;
|
||||
max-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
}
|
||||
html body > div.w-full.flex.flex-grow > div[class*="md:ml-64"],
|
||||
html body > div.w-full.flex > div[class*="md:ml-64"],
|
||||
body > div > div[class*="md:ml-64"],
|
||||
div[class*="md:ml-64"] {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
html body > div.w-full.flex.flex-grow > div[class*="rtl:md:mr-64"],
|
||||
html body > div.w-full.flex > div[class*="rtl:md:mr-64"],
|
||||
body > div > div[class*="rtl:md:mr-64"],
|
||||
div[class*="rtl:md:mr-64"] {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading skeleton animation */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.skeleton-loading {
|
||||
background: linear-gradient(90deg,
|
||||
transparent 25%,
|
||||
color-mix(in srgb, var(--color-base) 8%, transparent) 50%,
|
||||
transparent 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="hidden lg:block">
|
||||
@@ -3300,4 +3383,108 @@ window.saveServerIcon = function() {
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Smooth scroll to top on Livewire navigation (only for Proxmox pages)
|
||||
if (!window._proxmoxScrollListenerAdded) {
|
||||
window._proxmoxScrollListenerAdded = true;
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
// Only scroll to top if still on a Proxmox page
|
||||
if (document.querySelector('.proxmox-page-wrapper')) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard shortcuts for power actions (deduplicated)
|
||||
if (!window._proxmoxKeyboardListenerAdded) {
|
||||
window._proxmoxKeyboardListenerAdded = true;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Only if on a Proxmox page and no input is focused
|
||||
if (!document.querySelector('.proxmox-page-wrapper')) return;
|
||||
if (document.activeElement.tagName === 'INPUT' ||
|
||||
document.activeElement.tagName === 'TEXTAREA' ||
|
||||
document.activeElement.tagName === 'SELECT' ||
|
||||
document.activeElement.isContentEditable) return;
|
||||
|
||||
// Ctrl+Shift+S → Start server
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
||||
e.preventDefault();
|
||||
const startBtn = document.querySelector('[wire\\:click="start"]');
|
||||
if (startBtn && !startBtn.disabled) startBtn.click();
|
||||
}
|
||||
// Ctrl+Shift+K → Power off (force stop) — avoids Ctrl+Shift+P browser conflict
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'K') {
|
||||
e.preventDefault();
|
||||
const stopBtn = document.querySelector('[wire\\:click="kill"]');
|
||||
if (stopBtn && !stopBtn.disabled) stopBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Back to Top Button -->
|
||||
<div x-data="{ showBackToTop: false, _scrollHandler: null }"
|
||||
x-init="_scrollHandler = () => { showBackToTop = window.scrollY > 400 }; window.addEventListener('scroll', _scrollHandler, { passive: true })"
|
||||
x-destroy="window.removeEventListener('scroll', _scrollHandler)"
|
||||
class="fixed bottom-20 right-4 z-50">
|
||||
<button x-show="showBackToTop"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 translate-y-4"
|
||||
@click="window.scrollTo({ top: 0, behavior: 'smooth' })"
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full bg-primary/90 text-white shadow-lg hover:bg-primary hover:shadow-xl transition-all duration-200 backdrop-blur-sm"
|
||||
aria-label="Back to top">
|
||||
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification System -->
|
||||
<div x-data="{
|
||||
toasts: [],
|
||||
addToast(message, type = 'success') {
|
||||
const id = Date.now();
|
||||
this.toasts.push({ id, message, type });
|
||||
setTimeout(() => this.removeToast(id), 3000);
|
||||
},
|
||||
removeToast(id) {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
}
|
||||
}"
|
||||
@toast.window="addToast($event.detail.message, $event.detail.type || 'success')"
|
||||
class="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none">
|
||||
<template x-for="toast in toasts" :key="toast.id">
|
||||
<div x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-2 scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-2 scale-95"
|
||||
class="pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-xl shadow-lg border backdrop-blur-sm text-sm font-medium"
|
||||
:class="{
|
||||
'bg-success/10 border-success/30 text-success': toast.type === 'success',
|
||||
'bg-error/10 border-error/30 text-error': toast.type === 'error',
|
||||
'bg-warning/10 border-warning/30 text-warning': toast.type === 'warning',
|
||||
'bg-info/10 border-info/30 text-info': toast.type === 'info'
|
||||
}"
|
||||
@click="removeToast(toast.id)">
|
||||
<template x-if="toast.type === 'success'">
|
||||
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
</template>
|
||||
<template x-if="toast.type === 'error'">
|
||||
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</template>
|
||||
<template x-if="toast.type === 'warning'">
|
||||
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
</template>
|
||||
<template x-if="toast.type === 'info'">
|
||||
<svg class="size-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</template>
|
||||
<span x-text="toast.message"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user