496 lines
29 KiB
PHP
496 lines
29 KiB
PHP
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
|
|
<style>
|
|
.firewall-toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 44px;
|
|
height: 24px;
|
|
}
|
|
.firewall-toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.firewall-toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #e5e7eb;
|
|
transition: 0.3s;
|
|
border-radius: 24px;
|
|
}
|
|
.dark .firewall-toggle-slider {
|
|
background-color: #374151;
|
|
}
|
|
.firewall-toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 18px;
|
|
width: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: 0.3s;
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.firewall-toggle-switch input:checked + .firewall-toggle-slider {
|
|
background-color: #2563eb;
|
|
}
|
|
.firewall-toggle-switch input:checked + .firewall-toggle-slider:before {
|
|
transform: translateX(20px);
|
|
}
|
|
.firewall-toggle-switch input:disabled + .firewall-toggle-slider {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Error Message - Centered Modal -->
|
|
@if($error && $showErrorModal)
|
|
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/50 backdrop-blur-sm"
|
|
wire:click="$set('error', ''); $set('showErrorModal', false)">
|
|
<div class="bg-error/20 border-2 border-error rounded-lg p-6 shadow-lg max-w-3xl w-full mx-4 max-h-[80vh] overflow-y-auto" wire:click.stop>
|
|
<div class="flex items-start gap-3">
|
|
<x-ri-error-warning-fill class="size-6 text-error flex-shrink-0 mt-0.5" />
|
|
<div class="flex-1">
|
|
<h3 class="text-lg font-semibold text-error mb-2 text-center">Firewall Error</h3>
|
|
<div class="bg-background/50 rounded p-3 border border-error/30">
|
|
<p class="text-sm text-error/90 leading-relaxed whitespace-pre-wrap break-words">{{ $error }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
wire:click="$set('error', ''); $set('showErrorModal', false)"
|
|
class="text-error/70 hover:text-error transition-colors flex-shrink-0">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Firewall Status Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-shield-line class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Firewall Status</h2>
|
|
<p class="text-sm text-base/60 mt-1">Enable or disable the firewall for this virtual machine</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm font-medium text-base/70">Disabled</span>
|
|
<label class="firewall-toggle-switch" wire:loading.attr="disabled" wire:target="toggleFirewall">
|
|
<input
|
|
type="checkbox"
|
|
@if($firewallEnabled) checked @endif
|
|
wire:click="toggleFirewall"
|
|
wire:loading.attr="disabled">
|
|
<span class="firewall-toggle-slider"></span>
|
|
</label>
|
|
<span class="text-sm font-medium text-base/70">Enabled</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Adapters Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-wifi-line class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Network Adapters</h2>
|
|
<p class="text-sm text-base/60 mt-1">Enable or disable firewall for each network adapter</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if(count($networkAdapters) > 0)
|
|
<div class="space-y-4">
|
|
@foreach($networkAdapters as $adapter)
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-base font-semibold">{{ $adapter['name'] }}</h3>
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-primary/20 text-primary">
|
|
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
|
|
</span>
|
|
</div>
|
|
<div class="text-sm text-base/60 space-y-1">
|
|
@if($adapter['mac'])
|
|
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
|
|
@endif
|
|
</div>
|
|
<p class="text-xs text-base/50 mt-2">Enables or disables the firewall ability for this network adapter</p>
|
|
</div>
|
|
<div class="flex flex-col items-center gap-2 ml-4">
|
|
<span class="text-xs font-medium text-base/60">Firewall</span>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm font-medium text-base/70">Disabled</span>
|
|
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
|
<input
|
|
type="checkbox"
|
|
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
|
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
|
wire:loading.attr="disabled">
|
|
<span class="firewall-toggle-slider"></span>
|
|
</label>
|
|
<span class="text-sm font-medium text-base/70">Enabled</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-base/60">
|
|
<x-ri-wifi-line class="size-12 mx-auto mb-3 opacity-50" />
|
|
<p>No network adapters found</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($firewallEnabled)
|
|
<!-- Firewall Rules Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-list-check class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Firewall Rules</h2>
|
|
<p class="text-sm text-base/60 mt-1">Manage allow and block rules for network traffic</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
wire:click="startCreateRule"
|
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<x-ri-add-line class="size-4 inline mr-1" />
|
|
Add Rule
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Rule Form Modal -->
|
|
@if($showRuleForm)
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4 mb-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}</h3>
|
|
<button wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()" class="text-base/50 hover:text-base">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Action</label>
|
|
<select wire:model="ruleAction" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Protocol</label>
|
|
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
<option value="icmp">ICMP</option>
|
|
<option value="igmp">IGMP</option>
|
|
<option value="ggp">GGP</option>
|
|
<option value="ipencap">IPENCAP</option>
|
|
<option value="st">ST</option>
|
|
<option value="egp">EGP</option>
|
|
<option value="igp">IGP</option>
|
|
<option value="pip">PIP</option>
|
|
<option value="hmp">HMP</option>
|
|
<option value="xns-idp">XNS-IDP</option>
|
|
<option value="rdp">RDP</option>
|
|
<option value="iso-tp4">ISO-TP4</option>
|
|
<option value="dccp">DCCP</option>
|
|
<option value="xtp">XTP</option>
|
|
<option value="ddp">DDP</option>
|
|
<option value="idpr-cmtp">IDPR-CMTP</option>
|
|
<option value="ipv6">IPv6</option>
|
|
<option value="ipv6-route">IPv6-Route</option>
|
|
<option value="ipv6-frag">IPv6-Frag</option>
|
|
<option value="idrp">IDRP</option>
|
|
<option value="rsvp">RSVP</option>
|
|
<option value="gre">GRE</option>
|
|
<option value="esp">ESP</option>
|
|
<option value="ah">AH</option>
|
|
<option value="skip">SKIP</option>
|
|
<option value="ipv6-icmp">IPv6-ICMP</option>
|
|
<option value="ipv6-nonxt">IPv6-Nonxt</option>
|
|
<option value="ipv6-opts">IPv6-Opts</option>
|
|
<option value="vmtp">VMTP</option>
|
|
<option value="eigrp">EIGRP</option>
|
|
<option value="ospf">OSPF</option>
|
|
<option value="ax.25">AX.25</option>
|
|
<option value="ipip">IPIP</option>
|
|
<option value="etherip">EtherIP</option>
|
|
<option value="encap">ENCAP</option>
|
|
<option value="pim">PIM</option>
|
|
<option value="ipcomp">IPComp</option>
|
|
<option value="vrrp">VRRP</option>
|
|
<option value="l2tp">L2TP</option>
|
|
<option value="isis">ISIS</option>
|
|
<option value="sctp">SCTP</option>
|
|
<option value="fc">FC</option>
|
|
<option value="mobility-header">Mobility-Header</option>
|
|
<option value="udplite">UDPLite</option>
|
|
<option value="mpls-in-ip">MPLS-in-IP</option>
|
|
<option value="hip">HIP</option>
|
|
<option value="shim6">Shim6</option>
|
|
<option value="wesp">WESP</option>
|
|
<option value="rohc">ROHC</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source</label>
|
|
<input type="text" wire:model="ruleSource" placeholder="e.g., 192.168.1.0/24" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Destination</label>
|
|
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
@php
|
|
$allIps = collect();
|
|
if ($server->primaryIpv4) {
|
|
$allIps->push(['ip' => $server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
|
|
}
|
|
if ($server->primaryIpv6) {
|
|
$allIps->push(['ip' => $server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
|
|
}
|
|
foreach ($server->ipAddresses as $ipAddress) {
|
|
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
|
|
}
|
|
$uniqueIps = $allIps->unique('ip');
|
|
@endphp
|
|
@foreach($uniqueIps as $ipData)
|
|
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['type'] }} - {{ $ipData['label'] }})</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Destination Port</label>
|
|
<input type="text" wire:model="ruleDport" placeholder="e.g., 80,443 or 1000:2000" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source Port</label>
|
|
<input type="text" wire:model="ruleSport" placeholder="e.g., 8080" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Interface</label>
|
|
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
@foreach($networkAdapters as $adapter)
|
|
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Log Level</label>
|
|
<select wire:model="ruleLog" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="">No Logging</option>
|
|
<option value="nolog">No Log</option>
|
|
<option value="info">Info</option>
|
|
<option value="warning">Warning</option>
|
|
<option value="err">Error</option>
|
|
<option value="crit">Critical</option>
|
|
<option value="alert">Alert</option>
|
|
<option value="emerg">Emergency</option>
|
|
</select>
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium mb-2">Comment</label>
|
|
<input type="text" wire:model="ruleComment" placeholder="Optional description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div class="md:col-span-2 flex items-center gap-2">
|
|
<input type="checkbox" wire:model="ruleEnable" id="ruleEnable" class="rounded border-neutral">
|
|
<label for="ruleEnable" class="text-sm">Enable this rule</label>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 mt-4">
|
|
<button
|
|
wire:click="saveRule"
|
|
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">
|
|
Save Rule
|
|
</button>
|
|
<button
|
|
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()"
|
|
class="px-4 py-2 bg-background-secondary border border-neutral rounded-lg hover:bg-background transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Rules Table -->
|
|
@if(count($rules) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-neutral">
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Position</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Destination</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Protocol</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Port</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Status</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($rules as $rule)
|
|
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
|
<td class="py-3 px-4 text-sm">{{ $rule['pos'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="px-2 py-1 rounded text-xs font-medium
|
|
{{ ($rule['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
|
|
{{ ($rule['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
|
|
{{ ($rule['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
|
|
{{ $rule['action'] ?? '-' }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['source'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['dest'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ strtoupper($rule['proto'] ?? 'any') }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['dport'] ?? ($rule['sport'] ?? '-') }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
@if(isset($rule['enable']) && $rule['enable'] == 0)
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-base/20 text-base/60">Disabled</span>
|
|
@else
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-success/20 text-success">Enabled</span>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex gap-2">
|
|
<button
|
|
wire:click="toggleRule({{ $rule['pos'] ?? 0 }})"
|
|
wire:loading.attr="disabled"
|
|
class="transition-colors
|
|
{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'text-warning hover:text-warning/80' : 'text-success hover:text-success/80' }}"
|
|
title="{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'Enable rule' : 'Disable rule' }}">
|
|
@if(isset($rule['enable']) && $rule['enable'] == 0)
|
|
<x-ri-toggle-line class="size-4" />
|
|
@else
|
|
<x-ri-toggle-fill class="size-4" />
|
|
@endif
|
|
</button>
|
|
<button
|
|
wire:click="startEditRule({{ $rule['pos'] ?? 0 }})"
|
|
class="text-primary hover:text-primary/80 transition-colors"
|
|
title="Edit rule">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button
|
|
wire:click="deleteRule({{ $rule['pos'] ?? 0 }})"
|
|
wire:confirm="Are you sure you want to delete this rule?"
|
|
class="text-error hover:text-error/80 transition-colors"
|
|
title="Delete rule">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-base/60">
|
|
<x-ri-list-check class="size-12 mx-auto mb-3 opacity-50" />
|
|
<p>No firewall rules configured</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Firewall Logs Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-file-list-3-line class="size-5 text-primary" />
|
|
</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</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>
|
|
|
|
@if(count($firewallLogs) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-neutral">
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Timestamp</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Destination</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Protocol</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Source Port</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Dest Port</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">TTL</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">ACK</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($firewallLogs as $log)
|
|
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
|
<td class="py-3 px-4 text-sm">{{ $log['timestamp'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="px-2 py-1 rounded text-xs font-medium
|
|
{{ ($log['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
|
|
{{ ($log['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
|
|
{{ ($log['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
|
|
{{ $log['action'] ?? '-' }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm"><code class="text-xs">{{ $log['source'] ?? '-' }}</code></td>
|
|
<td class="py-3 px-4 text-sm"><code class="text-xs">{{ $log['destination'] ?? '-' }}</code></td>
|
|
<td class="py-3 px-4 text-sm">{{ $log['proto'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $log['spt'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $log['dpt'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $log['ttl'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $log['ack'] ?? '-' }}</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<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>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
|
|
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-50" />
|
|
<h3 class="text-lg font-semibold mb-2">Firewall is Disabled</h3>
|
|
<p class="text-base/60">Enable the firewall above to manage firewall rules.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
</x-proxmox::layout>
|
|
|