refactor: update Proxmox Firewall view to improve log loading functionality and user interaction, including a refresh button and enhanced error handling
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
||||
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling"false" :status-polling-interval="'2s'">
|
||||
|
||||
<style>
|
||||
.firewall-toggle-switch {
|
||||
@@ -298,138 +298,142 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Firewall Logs</h2>
|
||||
<p class="text-sm text-base/60 mt-1">Recent firewall activity and blocked/allowed connections (loaded on page load)</p>
|
||||
<p class="text-sm text-base/60 mt-1">Recent firewall activity and blocked/allowed connections</p>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<x-ri-refresh-line class="size-4 inline mr-1" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize firewall logs storage
|
||||
if (typeof window.firewallLogsData === 'undefined') {
|
||||
window.firewallLogsData = [];
|
||||
window.firewallLogsLoaded = false;
|
||||
window.firewallLogsError = '';
|
||||
}
|
||||
@if(!empty($logError))
|
||||
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-ri-error-warning-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-warning/90">{{ $logError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
// Listen for Livewire dispatch event
|
||||
Livewire.on('firewall-logs-loaded', (event) => {
|
||||
const data = event[0] || event;
|
||||
window.firewallLogsData = data.logs || [];
|
||||
window.firewallLogsError = data.error || '';
|
||||
window.firewallLogsLoaded = true;
|
||||
|
||||
// Trigger Alpine update
|
||||
const container = document.getElementById('firewall-logs-container');
|
||||
if (container && container.__x) {
|
||||
container.__x.$data.logs = window.firewallLogsData;
|
||||
container.__x.$data.error = window.firewallLogsError;
|
||||
container.__x.$data.loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="firewall-logs-container"
|
||||
x-data="{
|
||||
logs: window.firewallLogsData || [],
|
||||
loading: !window.firewallLogsLoaded,
|
||||
error: window.firewallLogsError || '',
|
||||
searchTerm: '',
|
||||
sortField: 'timestamp_iso',
|
||||
sortDirection: 'desc',
|
||||
async initializeLogs() {
|
||||
// Only load logs once on page load
|
||||
if (window.firewallLogsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
// Dispatch to Livewire to fetch logs
|
||||
@this.dispatch('fetchFirewallLogsAction');
|
||||
} catch (err) {
|
||||
this.error = 'Failed to load firewall logs: ' + (err.message || 'Unknown error');
|
||||
console.error('Error loading firewall logs:', err);
|
||||
}
|
||||
},
|
||||
sort(field) {
|
||||
if (this.sortField === field) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortField = field;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
},
|
||||
get filteredLogs() {
|
||||
const term = this.searchTerm.trim().toLowerCase();
|
||||
let entries = this.logs;
|
||||
|
||||
if (term) {
|
||||
entries = entries.filter((log) => {
|
||||
return Object.values(log).some((value) => {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
return String(value).toLowerCase().includes(term);
|
||||
@if(count($firewallLogs) > 0)
|
||||
<div
|
||||
x-data="{
|
||||
logs: [],
|
||||
searchTerm: '',
|
||||
sortField: 'timestamp_iso',
|
||||
sortDirection: 'desc',
|
||||
initialized: false,
|
||||
seenRaws: new Set(),
|
||||
initialize(initialLogs) {
|
||||
if (this.initialized) return;
|
||||
|
||||
if (initialLogs && initialLogs.length > 0) {
|
||||
this.logs = initialLogs.map((log, index) => ({
|
||||
index,
|
||||
...log,
|
||||
}));
|
||||
// Track which logs we've seen
|
||||
this.logs.forEach(log => {
|
||||
if (log.raw) this.seenRaws.add(log.raw);
|
||||
});
|
||||
this.initialized = true;
|
||||
}
|
||||
},
|
||||
updateLogs(newLogs) {
|
||||
if (!newLogs || newLogs.length === 0) return;
|
||||
|
||||
// Filter out logs we've already seen
|
||||
const logsToAdd = newLogs.filter(log => {
|
||||
if (!log.raw) return false;
|
||||
if (this.seenRaws.has(log.raw)) return false;
|
||||
this.seenRaws.add(log.raw);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (logsToAdd.length > 0) {
|
||||
// Add new logs to the beginning (most recent first)
|
||||
this.logs = [...logsToAdd, ...this.logs].map((log, index) => ({
|
||||
index,
|
||||
...log,
|
||||
}));
|
||||
}
|
||||
},
|
||||
sort(field) {
|
||||
if (this.sortField === field) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortField = field;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
},
|
||||
get filteredLogs() {
|
||||
const term = this.searchTerm.trim().toLowerCase();
|
||||
let entries = this.logs;
|
||||
|
||||
if (term) {
|
||||
entries = entries.filter((log) => {
|
||||
return Object.values(log).some((value) => {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
return String(value).toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const dir = this.sortDirection === 'asc' ? 1 : -1;
|
||||
const field = this.sortField;
|
||||
|
||||
return entries.slice().sort((a, b) => {
|
||||
let aVal = a[field] ?? '';
|
||||
let bVal = b[field] ?? '';
|
||||
|
||||
if (field === 'timestamp_iso') {
|
||||
const aTime = aVal ? new Date(aVal).getTime() : 0;
|
||||
const bTime = bVal ? new Date(bVal).getTime() : 0;
|
||||
return (aTime - bTime) * dir;
|
||||
}
|
||||
|
||||
if (!Number.isNaN(parseFloat(aVal)) && !Number.isNaN(parseFloat(bVal))) {
|
||||
return (parseFloat(aVal) - parseFloat(bVal)) * dir;
|
||||
}
|
||||
|
||||
return String(aVal).localeCompare(String(bVal)) * dir;
|
||||
});
|
||||
},
|
||||
}"
|
||||
x-init="initialize(@js($firewallLogs))"
|
||||
@logs-updated.window="updateLogs($event.detail.logs || $event.detail[0]?.logs || $event.detail[0] || [])"
|
||||
x-effect="
|
||||
// Watch for changes in the backend data and merge new logs
|
||||
const backendLogs = @js($firewallLogs);
|
||||
if (initialized && backendLogs && backendLogs.length > 0) {
|
||||
updateLogs(backendLogs);
|
||||
}
|
||||
|
||||
const dir = this.sortDirection === 'asc' ? 1 : -1;
|
||||
const field = this.sortField;
|
||||
|
||||
return entries.slice().sort((a, b) => {
|
||||
let aVal = a[field] ?? '';
|
||||
let bVal = b[field] ?? '';
|
||||
|
||||
if (field === 'timestamp_iso') {
|
||||
const aTime = aVal ? new Date(aVal).getTime() : 0;
|
||||
const bTime = bVal ? new Date(bVal).getTime() : 0;
|
||||
return (aTime - bTime) * dir;
|
||||
}
|
||||
|
||||
if (!Number.isNaN(parseFloat(aVal)) && !Number.isNaN(parseFloat(bVal))) {
|
||||
return (parseFloat(aVal) - parseFloat(bVal)) * dir;
|
||||
}
|
||||
|
||||
return String(aVal).localeCompare(String(bVal)) * dir;
|
||||
});
|
||||
},
|
||||
}"
|
||||
x-init="initializeLogs()"
|
||||
>
|
||||
<div x-show="loading" x-cloak class="text-center py-8 text-base/60">
|
||||
<div class="animate-spin inline-block">
|
||||
<x-ri-loader-4-line class="size-8 mx-auto mb-3 opacity-50" />
|
||||
"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div class="relative w-full md:w-72">
|
||||
<x-ri-search-line class="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-base/40" />
|
||||
<input
|
||||
type="text"
|
||||
x-model="searchTerm"
|
||||
placeholder="Search logs..."
|
||||
class="firewall-log-search-input w-full pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40"
|
||||
/>
|
||||
</div>
|
||||
<p>Loading firewall logs...</p>
|
||||
</div>
|
||||
|
||||
<div x-show="error" x-cloak class="bg-error/10 border border-error/30 rounded-lg p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-ri-error-warning-line class="size-5 text-error flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-error/90" x-text="error"></p>
|
||||
</div>
|
||||
<div class="text-xs text-base/50">
|
||||
<span x-text="`${filteredLogs.length} / ${logs.length} results`"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template x-if="!loading && !error && logs.length > 0">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div class="relative w-full md:w-72">
|
||||
<x-ri-search-line class="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-base/40" />
|
||||
<input
|
||||
type="text"
|
||||
x-model="searchTerm"
|
||||
placeholder="Search logs..."
|
||||
class="firewall-log-search-input w-full pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-base/50">
|
||||
<span x-text="`${filteredLogs.length} / ${logs.length} results`"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
@@ -1032,17 +1036,14 @@
|
||||
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No logs matched your search.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && !error && logs.length === 0">
|
||||
<div class="text-center py-8 text-base/60">
|
||||
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No firewall logs available</p>
|
||||
<p class="text-xs mt-2">Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options)</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@elseif(empty($logError))
|
||||
<div class="text-center py-8 text-base/60">
|
||||
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No firewall logs available</p>
|
||||
<p class="text-xs mt-2">Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options)</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
|
||||
@@ -1452,4 +1453,3 @@
|
||||
</script>
|
||||
|
||||
</x-proxmox::layout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user