feat: enhance log display in Proxmox Firewall component by improving the layout and interactivity of log entries, including IP geolocation modals and expanded details for better user experience
This commit is contained in:
@@ -455,54 +455,293 @@
|
||||
<template x-if="true">
|
||||
<template x-if="log.is_group">
|
||||
<!-- Grouped log row -->
|
||||
<tr x-data="{ expanded: false }" class="border-b border-neutral/20">
|
||||
<td colspan="8" class="p-0">
|
||||
<div class="hover:bg-background-secondary/50">
|
||||
<div class="flex items-center py-3 px-4 cursor-pointer" @click="expanded = !expanded">
|
||||
<div class="flex-1 grid grid-cols-8 gap-4 items-center">
|
||||
<div class="text-sm" x-text="log.timestamp_display || '-'"></div>
|
||||
<div class="text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-'"></span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-'"></span>
|
||||
</div>
|
||||
<div class="text-sm" x-text="log.protocol || '-'"></div>
|
||||
<div class="text-sm font-mono" x-text="log.source || '-'"></div>
|
||||
<div class="text-sm" x-text="log.source_port || '-'"></div>
|
||||
<div class="text-sm font-mono" x-text="log.destination || '-'"></div>
|
||||
<div class="text-sm" x-text="log.destination_port || '-'"></div>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-primary/10 text-primary rounded text-xs font-semibold" x-text="`${log.group_count}x`"></span>
|
||||
<svg class="size-4 text-base/50 transition-transform" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<template x-data="{ expanded: false }">
|
||||
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50 cursor-pointer" @click="expanded = !expanded">
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="size-3 text-base/50 transition-transform flex-shrink-0" :class="{ 'rotate-90': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<span x-text="log.timestamp_display || '-'"></span>
|
||||
</div>
|
||||
<!-- Expanded details -->
|
||||
<div x-show="expanded" x-collapse class="border-t border-neutral/20 bg-background/50">
|
||||
<div class="p-4 space-y-2">
|
||||
<div class="text-xs text-base/60 mb-2">Showing <span x-text="log.group_count"></span> similar events:</div>
|
||||
<div class="max-h-96 overflow-y-auto space-y-1">
|
||||
<template x-for="(subLog, subIndex) in log.grouped_logs" :key="subIndex">
|
||||
<div class="text-xs font-mono bg-background-secondary/50 p-2 rounded border border-neutral/10">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-base/60" x-text="subLog.timestamp_display"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase" :class="subLog.level_badge_class" x-text="subLog.level_label"></span>
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold" :class="subLog.action_badge_class" x-text="subLog.action"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold uppercase" :class="log.level_badge_class" x-text="log.level_label || '-'"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-'"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<span x-text="log.protocol || '-'"></span>
|
||||
<template x-if="log.protocol && log.protocol !== '-'">
|
||||
<div class="relative" x-data="{ open: false, protocolDescription: window.getProtocolDescription(log.protocol) }" x-cloak>
|
||||
<button type="button" @click.stop="open = true" class="text-base/50 hover:text-base/70 transition-colors">
|
||||
<x-ri-information-line class="size-3" />
|
||||
</button>
|
||||
<!-- Modal -->
|
||||
<div x-show="open"
|
||||
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-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.stop="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
style="display: none;">
|
||||
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
|
||||
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="p-6 break-words">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<x-ri-information-line class="size-5 text-primary flex-shrink-0" />
|
||||
<h3 class="text-lg font-semibold break-words">Protocol: <span class="font-mono break-all" x-text="log.protocol"></span></h3>
|
||||
</div>
|
||||
<div class="text-sm text-base/80 prose prose-sm max-w-none" x-html="protocolDescription"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-mono" x-text="log.source || '-'"></span>
|
||||
<template x-if="log.source && log.source !== '-' && window.isPublicIP(log.source)">
|
||||
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.source }" x-cloak>
|
||||
<button type="button" @click.stop="
|
||||
if (!ipData && !loading) {
|
||||
loading = true;
|
||||
open = true;
|
||||
window.fetchIPLocation(ip).then(data => {
|
||||
ipData = data;
|
||||
loading = false;
|
||||
}).catch(err => {
|
||||
ipData = { status: 'error', message: 'Failed to load location data' };
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
open = true;
|
||||
}
|
||||
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
|
||||
<x-ri-map-pin-line class="size-3" />
|
||||
</button>
|
||||
<!-- Modal -->
|
||||
<div x-show="open"
|
||||
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-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.stop="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
style="display: none;">
|
||||
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
|
||||
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="p-6 break-words">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
|
||||
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all" x-text="log.source"></span></h3>
|
||||
</div>
|
||||
<div x-show="loading" class="text-center py-8">
|
||||
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
|
||||
<g>
|
||||
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="text-sm text-base/60">Loading location data...</p>
|
||||
</div>
|
||||
<div x-show="ipData && !loading">
|
||||
<template x-if="ipData && (ipData.status === 'success')">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div class="space-y-2 break-words">
|
||||
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
|
||||
</div>
|
||||
<div class="space-y-2 break-words">
|
||||
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full">
|
||||
<p class="text-xs text-base/60 mb-2">Location Map:</p>
|
||||
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
|
||||
<iframe
|
||||
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
|
||||
style="width: 100%; height: 100%; border: 0;"
|
||||
frameborder="0"
|
||||
loading="lazy"
|
||||
allowfullscreen
|
||||
title="Location Map"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base/70 break-all" x-text="subLog.raw"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" x-text="log.source_port || '-'"></td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-mono" x-text="log.destination || '-'"></span>
|
||||
<template x-if="log.destination && log.destination !== '-' && window.isPublicIP(log.destination)">
|
||||
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.destination }" x-cloak>
|
||||
<button type="button" @click.stop="
|
||||
if (!ipData && !loading) {
|
||||
loading = true;
|
||||
open = true;
|
||||
window.fetchIPLocation(ip).then(data => {
|
||||
ipData = data;
|
||||
loading = false;
|
||||
}).catch(err => {
|
||||
ipData = { status: 'error', message: 'Failed to load location data' };
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
open = true;
|
||||
}
|
||||
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
|
||||
<x-ri-map-pin-line class="size-3" />
|
||||
</button>
|
||||
<!-- Modal -->
|
||||
<div x-show="open"
|
||||
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-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.stop="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
style="display: none;">
|
||||
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
|
||||
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="p-6 break-words">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
|
||||
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all" x-text="log.destination"></span></h3>
|
||||
</div>
|
||||
<div x-show="loading" class="text-center py-8">
|
||||
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
|
||||
<g>
|
||||
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="text-sm text-base/60">Loading location data...</p>
|
||||
</div>
|
||||
<div x-show="ipData && !loading">
|
||||
<template x-if="ipData && (ipData.status === 'success')">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div class="space-y-2 break-words">
|
||||
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
|
||||
</div>
|
||||
<div class="space-y-2 break-words">
|
||||
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
|
||||
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full">
|
||||
<p class="text-xs text-base/60 mb-2">Location Map:</p>
|
||||
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
|
||||
<iframe
|
||||
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
|
||||
style="width: 100%; height: 100%; border: 0;"
|
||||
frameborder="0"
|
||||
loading="lazy"
|
||||
allowfullscreen
|
||||
title="Location Map"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
|
||||
<div class="text-center py-4">
|
||||
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span x-text="log.destination_port || '-'"></span>
|
||||
<span class="px-2 py-1 bg-primary/10 text-primary rounded-full text-xs font-semibold" x-text="log.group_count"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Expanded details row -->
|
||||
<tr x-show="expanded" x-collapse class="border-b border-neutral/20">
|
||||
<td colspan="8" class="p-0">
|
||||
<div class="bg-background/30 px-4 py-3">
|
||||
<div class="text-xs text-base/60 mb-3 font-semibold">
|
||||
<span x-text="log.group_count"></span> similar events
|
||||
</div>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<template x-for="(subLog, subIndex) in log.grouped_logs" :key="subIndex">
|
||||
<div class="bg-background-secondary border border-neutral/10 rounded-lg p-3">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="text-xs text-base/70" x-text="subLog.timestamp_display"></span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-semibold uppercase" :class="subLog.level_badge_class" x-text="subLog.level_label"></span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-semibold" :class="subLog.action_badge_class" x-text="subLog.action"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs mb-2">
|
||||
<div><span class="text-base/50">Protocol:</span> <span class="font-semibold" x-text="subLog.protocol || '-'"></span></div>
|
||||
<div><span class="text-base/50">Interface:</span> <span class="font-semibold" x-text="subLog.iface || '-'"></span></div>
|
||||
<div><span class="text-base/50">Source:</span> <span class="font-mono font-semibold" x-text="subLog.source || '-'"></span><span x-show="subLog.source_port" class="text-base/50">:<span class="font-semibold" x-text="subLog.source_port"></span></span></div>
|
||||
<div><span class="text-base/50">Destination:</span> <span class="font-mono font-semibold" x-text="subLog.destination || '-'"></span><span x-show="subLog.destination_port" class="text-base/50">:<span class="font-semibold" x-text="subLog.destination_port"></span></span></div>
|
||||
</div>
|
||||
<details class="text-xs">
|
||||
<summary class="cursor-pointer text-base/50 hover:text-base/70 transition-colors">View raw log</summary>
|
||||
<div class="mt-2 p-2 bg-background/50 rounded border border-neutral/10 font-mono text-[10px] text-base/60 break-all" x-text="subLog.raw"></div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
<template x-if="!log.is_group">
|
||||
<!-- Single log row -->
|
||||
|
||||
Reference in New Issue
Block a user