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:
@@ -8,23 +8,19 @@
|
||||
<script data-navigate-track>
|
||||
// Define copyToClipboard only if not already defined (prevent duplicates on navigation)
|
||||
if (typeof window.copyToClipboard === 'undefined') {
|
||||
window.copyToClipboard = function(text, buttonEl) {
|
||||
window.copyToClipboard = function(text, el) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Change button style & text
|
||||
const originalText = buttonEl.innerText;
|
||||
buttonEl.innerText = "Copied!";
|
||||
buttonEl.classList.remove("bg-blue-600", "hover:bg-blue-700");
|
||||
buttonEl.classList.add("bg-green-600", "hover:bg-green-700");
|
||||
|
||||
// Revert after 2s
|
||||
setTimeout(() => {
|
||||
buttonEl.innerText = originalText;
|
||||
buttonEl.classList.remove("bg-green-600", "hover:bg-green-700");
|
||||
buttonEl.classList.add("bg-blue-600", "hover:bg-blue-700");
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error("Failed to copy: ", err);
|
||||
});
|
||||
// Show brief visual feedback on the button
|
||||
const icon = el.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.classList.add('text-success');
|
||||
setTimeout(() => icon.classList.remove('text-success'), 1500);
|
||||
}
|
||||
// Dispatch a custom event for toast notification
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Copied to clipboard', type: 'success' }
|
||||
}));
|
||||
}).catch(() => {});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -187,10 +183,10 @@
|
||||
</div>
|
||||
|
||||
@php
|
||||
$cpuCores = max(1, (int)($status->cpus ?? 1));
|
||||
$cpuUsagePercent = round(max(0, ($status->cpu ?? 0) * 100), 1);
|
||||
$memoryTotal = (int)($status->maxmem ?? 0);
|
||||
$memoryUsed = (int)($status->mem ?? 0);
|
||||
$cpuCores = max(1, (int)($status?->cpus ?? 1));
|
||||
$cpuUsagePercent = round(max(0, ($status?->cpu ?? 0) * 100), 1);
|
||||
$memoryTotal = (int)($status?->maxmem ?? 0);
|
||||
$memoryUsed = (int)($status?->mem ?? 0);
|
||||
$memoryPercent = $memoryTotal > 0 ? round(($memoryUsed / $memoryTotal) * 100, 1) : 0;
|
||||
$manualSnapshotLimit = 5;
|
||||
$manualSnapshotUsed = $server->statistics()->where('is_automatic', false)->count();
|
||||
@@ -228,7 +224,7 @@
|
||||
[
|
||||
'label' => 'CPU Allocation',
|
||||
'used_percent' => $cpuUsagePercent,
|
||||
'usage_summary' => number_format(($status->cpu ?? 0) * $cpuCores, 2) . ' / ' . $cpuCores . ' vCPU',
|
||||
'usage_summary' => number_format(($status?->cpu ?? 0) * $cpuCores, 2) . ' / ' . $cpuCores . ' vCPU',
|
||||
'entitlement_label' => $cpuCores . ' vCPU entitlement',
|
||||
'description' => 'Live hypervisor usage sampled directly from the cluster.',
|
||||
'gradient' => 'linear-gradient(90deg, #38bdf8 0%, #6366f1 100%)',
|
||||
@@ -274,7 +270,7 @@
|
||||
};
|
||||
$rateScale = 5 * 1024 * 1024; // 5 MiB/s caps the bar width
|
||||
$aggregateRate = ($networkRates['in_rate'] ?? 0) + ($networkRates['out_rate'] ?? 0);
|
||||
$totalNetworkBytes = (int) (($status->netin ?? 0) + ($status->netout ?? 0));
|
||||
$totalNetworkBytes = (int) (($status?->netin ?? 0) + ($status?->netout ?? 0));
|
||||
$totalNetworkFormatted = number_format($totalNetworkBytes / 1024 / 1024, 2);
|
||||
$lastSampleTimestamp = now()->toIso8601String();
|
||||
@endphp
|
||||
@@ -388,7 +384,15 @@
|
||||
<div class="">
|
||||
|
||||
<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 class="flex items-center gap-3 p-4 rounded-t-lg border-t border-x border-neutral bg-gradient-to-r from-primary/10 via-primary/5 to-transparent">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/15">
|
||||
<x-ri-server-line class="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">Server Details</h3>
|
||||
<p class="text-xs text-base/50">Configuration and access information</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Section -->
|
||||
<div class="bg-background-secondary border-x border-neutral p-4 space-y-4 rounded-m-lg">
|
||||
@@ -430,7 +434,7 @@
|
||||
<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' }}
|
||||
{{ $server->hostname ?? $status?->name ?? 'N/A' }}
|
||||
</span>
|
||||
<button wire:click="startEditingHostname" type="button"
|
||||
wire:loading.attr="disabled"
|
||||
@@ -514,28 +518,14 @@
|
||||
<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);
|
||||
}
|
||||
}">
|
||||
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80 group">
|
||||
<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"
|
||||
<button @click="copyToClipboard('{{ $server->primaryIpv4->ip }}', $el)" type="button"
|
||||
class="text-base/50 hover:text-primary transition-all p-0.5 rounded"
|
||||
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>
|
||||
<x-ri-file-copy-line class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -547,28 +537,14 @@
|
||||
<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);
|
||||
}
|
||||
}">
|
||||
<div class="flex items-center gap-2 border border-neutral rounded-md bg-background px-3 py-2 text-sm text-base/80 group">
|
||||
<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"
|
||||
<button @click="copyToClipboard('{{ $server->primaryIpv6->ip }}', $el)" type="button"
|
||||
class="text-base/50 hover:text-primary transition-all p-0.5 rounded"
|
||||
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>
|
||||
<x-ri-file-copy-line class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -856,14 +832,20 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- DNS Section Wrapper -->
|
||||
@if($server->primaryIpv6)
|
||||
<div>
|
||||
<div style="background-image: url('https://billing.virtnet.bond/assets/images/dns.jpeg'); background-size: 100%; height: 5.5vh;" class="flex flex-col p-4 rounded-t-lg border-t border-x border-neutral "></div>
|
||||
<div class="flex items-center gap-3 p-4 rounded-t-lg border-t border-x border-neutral bg-gradient-to-r from-info/10 via-info/5 to-transparent">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-info/15">
|
||||
<x-ri-global-line class="size-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">Domain Name System (DNS)</h3>
|
||||
<p class="text-xs text-base/50">IPv6 DNS records for your server</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 bg-background-secondary hover:bg-background-secondary/80 border-x border-neutral p-4 ">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<h2 class="text-lg font-semibold break-words">
|
||||
Domain Name System (DNS)
|
||||
</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" />
|
||||
@@ -917,126 +899,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Explanation -->
|
||||
<!-- DNS Records -->
|
||||
<p class="text-sm text-base/60 mb-1">IPv6-based DNS records for <span class="font-mono text-xs">{{ $server->primaryIpv6->ip }}</span></p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-md font-semibold text-base/60">
|
||||
<p>IPv6 DNS: {{ $server->primaryIpv6->ip }}</p>
|
||||
|
||||
<!-- Virtnet -->
|
||||
<div class="flex items-center space-x-2 ml-8">
|
||||
<span>
|
||||
{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.virtnet.host' }}
|
||||
<small>(Internally Provided)</small>
|
||||
</span>
|
||||
<div x-data="{
|
||||
copied: false,
|
||||
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.virtnet.host' }}',
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.copyText);
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 3000);
|
||||
}
|
||||
}" class="flex items-center">
|
||||
<button @click="copyToClipboard()" type="button"
|
||||
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
|
||||
<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 class="divide-y divide-neutral rounded-lg border border-neutral overflow-hidden bg-background">
|
||||
@foreach([
|
||||
['label' => 'Virtnet DNS', 'host' => str_replace(':', '-', $server->primaryIpv6->ip) . '.virtnet.host', 'provider' => 'Internally Provided'],
|
||||
['label' => 'nip.io', 'host' => str_replace(':', '-', $server->primaryIpv6->ip) . '.nip.io', 'provider' => 'nip.io', 'url' => 'https://nip.io'],
|
||||
['label' => 'sslip.io', 'host' => str_replace(':', '-', $server->primaryIpv6->ip) . '.sslip.io', 'provider' => 'sslip.io', 'url' => 'https://sslip.io'],
|
||||
['label' => 'backname.io', 'host' => str_replace(':', '-', $server->primaryIpv6->ip) . '.backname.io', 'provider' => 'backname.io', 'url' => 'https://backname.io'],
|
||||
] as $dns)
|
||||
<div class="flex items-center justify-between px-3 py-2.5 group hover:bg-background-secondary/50 transition-colors">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium font-mono truncate">{{ $dns['host'] }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-base/50">
|
||||
@if(isset($dns['url']))
|
||||
<a href="{{ $dns['url'] }}" target="_blank" class="hover:text-primary transition-colors">{{ $dns['provider'] }} ↗</a>
|
||||
@else
|
||||
{{ $dns['provider'] }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" @click="copyToClipboard('{{ $dns['host'] }}', $el)"
|
||||
class="opacity-0 group-hover:opacity-100 text-base/50 hover:text-primary transition-all p-1.5 rounded-md hover:bg-primary/10"
|
||||
aria-label="Copy {{ $dns['label'] }}">
|
||||
<x-ri-file-copy-line class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- nip.io -->
|
||||
<div class="flex items-center space-x-2 ml-8">
|
||||
<span>
|
||||
{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.nip.io' }}
|
||||
<small>(Hosted by <a href="https://nip.io" target="_blank">nip.io</a>)</small>
|
||||
</span>
|
||||
<div x-data="{
|
||||
copied: false,
|
||||
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.nip.io' }}',
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.copyText);
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 3000);
|
||||
}
|
||||
}" class="flex items-center">
|
||||
<button @click="copyToClipboard()" type="button"
|
||||
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
|
||||
<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>
|
||||
|
||||
<!-- sslip.io -->
|
||||
<div class="flex items-center space-x-2 ml-8">
|
||||
<span>
|
||||
{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.sslip.io' }}
|
||||
<small>(Hosted by <a href="https://sslip.io" target="_blank">sslip.io</a>)</small>
|
||||
</span>
|
||||
<div x-data="{
|
||||
copied: false,
|
||||
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.sslip.io' }}',
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.copyText);
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 3000);
|
||||
}
|
||||
}" class="flex items-center">
|
||||
<button @click="copyToClipboard()" type="button"
|
||||
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
|
||||
<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>
|
||||
|
||||
<!-- backname.io -->
|
||||
<div class="flex items-center space-x-2 ml-8">
|
||||
<span>
|
||||
{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.backname.io' }}
|
||||
<small>(Hosted by <a href="https://backname.io" target="_blank">backname.io</a>)</small>
|
||||
</span>
|
||||
<div x-data="{
|
||||
copied: false,
|
||||
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.backname.io' }}',
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.copyText);
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 3000);
|
||||
}
|
||||
}" class="flex items-center">
|
||||
<button @click="copyToClipboard()" type="button"
|
||||
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
|
||||
|
||||
@@ -1059,6 +953,7 @@
|
||||
</p></div></div>
|
||||
|
||||
<!-- End DNS Section Wrapper -->
|
||||
@endif
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div id="notes-section" class="bg-background-secondary hover:bg-background-secondary/80 border border-neutral rounded-xl p-6 flex flex-col" x-data="{ showExportErrorModal: false }" x-cloak>
|
||||
@@ -1232,13 +1127,18 @@
|
||||
|
||||
@if(stripos($server->os?->name ?? '', 'Windows') !== false)
|
||||
<div class="">
|
||||
<div style="background-image: url('https://billing.virtnet.bond/assets/images/windows.jpg'); background-size: 100%; height: 5.5vh;" class="flex flex-col p-4 rounded-t-lg border border-neutral"></div>
|
||||
<div class="flex items-center gap-3 p-4 rounded-t-lg border-t border-x border-neutral bg-gradient-to-r from-info/10 via-info/5 to-transparent">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-info/15">
|
||||
<x-ri-windows-line class="size-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">Windows Server</h3>
|
||||
<p class="text-xs text-base/50">Installation and configuration notes</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-info/10 border border-info/30 p-4 rounded-b-lg">
|
||||
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<h2 class="text-lg font-semibold break-words">
|
||||
Windows Server Installations
|
||||
</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" />
|
||||
@@ -1470,11 +1370,12 @@
|
||||
});
|
||||
|
||||
const formatBytes = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!bytes || bytes <= 0 || !isFinite(bytes)) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
if (i < 0 || i >= sizes.length) return '0 B';
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
@@ -2259,6 +2160,7 @@
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
window.downloadText = function(textContent, filename) {
|
||||
@@ -2271,6 +2173,7 @@
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const sanitizeCellText = (value) => {
|
||||
@@ -2286,10 +2189,11 @@
|
||||
};
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!bytes || bytes <= 0 || !isFinite(bytes)) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
if (i < 0 || i >= sizes.length) return '0 B';
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user