feat: add pagination for firewall logs in Proxmox component, enhancing log management and user experience with navigation controls
This commit is contained in:
@@ -157,6 +157,10 @@ class Firewall extends Component
|
|||||||
public array $firewallLogs = [];
|
public array $firewallLogs = [];
|
||||||
public int $logLimit = 1000;
|
public int $logLimit = 1000;
|
||||||
public string $logError = '';
|
public string $logError = '';
|
||||||
|
|
||||||
|
// Pagination for logs
|
||||||
|
public int $currentPage = 1;
|
||||||
|
public int $perPage = 50;
|
||||||
|
|
||||||
// Available IPs for rules
|
// Available IPs for rules
|
||||||
public array $availableIps = [];
|
public array $availableIps = [];
|
||||||
@@ -1239,6 +1243,50 @@ class Firewall extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== LOG PAGINATION ====================
|
||||||
|
|
||||||
|
public function getPaginatedLogsProperty(): array
|
||||||
|
{
|
||||||
|
$reversedLogs = array_reverse($this->firewallLogs);
|
||||||
|
$offset = ($this->currentPage - 1) * $this->perPage;
|
||||||
|
return array_slice($reversedLogs, $offset, $this->perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalPagesProperty(): int
|
||||||
|
{
|
||||||
|
return (int)ceil(count($this->firewallLogs) / $this->perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToPage(int $page): void
|
||||||
|
{
|
||||||
|
$totalPages = $this->getTotalPagesProperty();
|
||||||
|
if ($page >= 1 && $page <= $totalPages) {
|
||||||
|
$this->currentPage = $page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function previousPage(): void
|
||||||
|
{
|
||||||
|
if ($this->currentPage > 1) {
|
||||||
|
$this->currentPage--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextPage(): void
|
||||||
|
{
|
||||||
|
if ($this->currentPage < $this->getTotalPagesProperty()) {
|
||||||
|
$this->currentPage++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedPerPage(): void
|
||||||
|
{
|
||||||
|
// Reset to page 1 when changing per-page value
|
||||||
|
$this->currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LOG PARSING ====================
|
||||||
|
|
||||||
protected function parseFirewallLogs(array $rawLogs): array
|
protected function parseFirewallLogs(array $rawLogs): array
|
||||||
{
|
{
|
||||||
$parsed = [];
|
$parsed = [];
|
||||||
|
|||||||
@@ -730,7 +730,36 @@
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
||||||
<button type="button" wire:click="loadFirewallLogs" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
<button type="button" wire:click="loadFirewallLogs" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
||||||
<x-ri-refresh-line class="size-4" wire:loading.class="animate-spin" wire:target="loadFirewallLogs" />
|
<span wire:loading.remove wire:target="loadFirewallLogs">
|
||||||
|
<x-ri-refresh-line class="size-4" />
|
||||||
|
</span>
|
||||||
|
<div wire:loading wire:target="loadFirewallLogs" class="loader-container">
|
||||||
|
<style>
|
||||||
|
.loader-container {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
width: 16px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 2px;
|
||||||
|
background:
|
||||||
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
|
||||||
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
|
||||||
|
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation: loader-spin 1s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes loader-spin {
|
||||||
|
100% { transform: rotate(1turn); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span>
|
<span wire:loading.remove wire:target="loadFirewallLogs">Refresh</span>
|
||||||
<span wire:loading wire:target="loadFirewallLogs">Loading...</span>
|
<span wire:loading wire:target="loadFirewallLogs">Loading...</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -759,7 +788,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach(array_slice(array_reverse($firewallLogs), 0, 50) as $log)
|
@foreach($this->paginatedLogs as $log)
|
||||||
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
|
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
|
||||||
<td class="py-3 px-4 text-xs text-base/70">{{ $log['timestamp'] ?? '-' }}</td>
|
<td class="py-3 px-4 text-xs text-base/70">{{ $log['timestamp'] ?? '-' }}</td>
|
||||||
<td class="py-3 px-4 text-sm">
|
<td class="py-3 px-4 text-sm">
|
||||||
@@ -933,12 +962,97 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@if(count($firewallLogs) > 50)
|
|
||||||
<div class="mt-4 text-center text-sm text-base/60">
|
{{-- Pagination Controls --}}
|
||||||
Showing latest 50 of {{ count($firewallLogs) }} log entries (most recent first)
|
<div class="mt-6 flex items-center justify-between border-t border-neutral/20 pt-4">
|
||||||
</div>
|
{{-- Records per page selector --}}
|
||||||
@endif
|
<div class="flex items-center gap-3">
|
||||||
|
<label class="text-sm text-base/70">Show:</label>
|
||||||
|
<select wire:model.live="perPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
<option value="250">250</option>
|
||||||
|
</select>
|
||||||
|
<span class="text-sm text-base/60">per page</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Page info and navigation --}}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-sm text-base/60">
|
||||||
|
Showing {{ (($currentPage - 1) * $perPage) + 1 }} - {{ min($currentPage * $perPage, count($firewallLogs)) }} of {{ count($firewallLogs) }} logs
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{{-- First page --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="goToPage(1)"
|
||||||
|
@if($currentPage === 1) disabled @endif
|
||||||
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="First page">
|
||||||
|
<x-ri-arrow-left-double-line class="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Previous page --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="previousPage"
|
||||||
|
@if($currentPage === 1) disabled @endif
|
||||||
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Previous page">
|
||||||
|
<x-ri-arrow-left-s-line class="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Page numbers --}}
|
||||||
|
@php
|
||||||
|
$totalPages = $this->totalPages;
|
||||||
|
$startPage = max(1, $currentPage - 2);
|
||||||
|
$endPage = min($totalPages, $currentPage + 2);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($startPage > 1)
|
||||||
|
<button type="button" wire:click="goToPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
|
||||||
|
@if($startPage > 2)
|
||||||
|
<span class="px-2 text-base/60">...</span>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@for($i = $startPage; $i <= $endPage; $i++)
|
||||||
|
<button type="button"
|
||||||
|
wire:click="goToPage({{ $i }})"
|
||||||
|
class="px-3 py-1 border rounded text-sm transition-colors {{ $currentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
|
||||||
|
{{ $i }}
|
||||||
|
</button>
|
||||||
|
@endfor
|
||||||
|
|
||||||
|
@if($endPage < $totalPages)
|
||||||
|
@if($endPage < $totalPages - 1)
|
||||||
|
<span class="px-2 text-base/60">...</span>
|
||||||
|
@endif
|
||||||
|
<button type="button" wire:click="goToPage({{ $totalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $totalPages }}</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Next page --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="nextPage"
|
||||||
|
@if($currentPage === $totalPages) disabled @endif
|
||||||
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Next page">
|
||||||
|
<x-ri-arrow-right-s-line class="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- Last page --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="goToPage({{ $totalPages }})"
|
||||||
|
@if($currentPage === $totalPages) disabled @endif
|
||||||
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Last page">
|
||||||
|
<x-ri-arrow-right-double-line class="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="text-center py-12 text-base/60">
|
<div class="text-center py-12 text-base/60">
|
||||||
<x-ri-file-list-3-line class="size-16 mx-auto mb-4 opacity-30" />
|
<x-ri-file-list-3-line class="size-16 mx-auto mb-4 opacity-30" />
|
||||||
|
|||||||
Reference in New Issue
Block a user