ui(servers): full-width lone firewall/fail2ban panel with internal 2-col layout
On the Server-Details security section, a single installed tool was capped at lg:max-w-2xl and left-aligned, leaving a large void to its right. Now a lone panel spans the full content width and splits its own body into two columns on lg ($lone flag): firewall = defaults/state | rules list; fail2ban = status summary + view-bans | whitelist (ignoreip). Two tools keep the side-by-side lg:grid-cols-2 grid. All wire:click / $dispatch bindings, buttons, and __() keys preserved; no new lang keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
7e1d342844
commit
02d74bb147
|
|
@ -276,14 +276,20 @@
|
|||
$showFw = $fwSupported && ($fwInstalled || $fwReadError);
|
||||
$showF2b = $f2bSupported && ($f2bInstalled || $f2bReadError);
|
||||
$panelCount = ($showFw ? 1 : 0) + ($showF2b ? 1 : 0);
|
||||
|
||||
// A single installed tool would otherwise sit in a capped, left-aligned box
|
||||
// beside a large void. Instead the lone panel spans the full content width and
|
||||
// splits its OWN body into two columns on lg — a rich, deliberate card rather
|
||||
// than a fragment. Two tools keep the side-by-side grid.
|
||||
$lone = $panelCount === 1;
|
||||
@endphp
|
||||
|
||||
@if ($panelCount > 0)
|
||||
{{-- Two tools → side-by-side; a lone tool is capped so it doesn't stretch edge-to-edge. --}}
|
||||
{{-- Two tools → side-by-side; a lone tool spans full width and lays its body out
|
||||
internally in two columns (see $lone), so neither case leaves dead space. --}}
|
||||
<div @class([
|
||||
'grid grid-cols-1 gap-3',
|
||||
'lg:grid-cols-2 lg:items-start' => $panelCount === 2,
|
||||
'lg:max-w-2xl' => $panelCount === 1,
|
||||
])>
|
||||
@if ($showFw)
|
||||
{{-- Firewall-Regeln --}}
|
||||
|
|
@ -313,26 +319,31 @@
|
|||
</x-btn>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-wrap items-center gap-2 border-b border-line px-4 py-2.5 sm:px-5">
|
||||
@if ($fwTool === 'ufw')
|
||||
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_default') }}</span>
|
||||
<x-badge tone="neutral">{{ __('servers.firewall_incoming', ['value' => $fwDefaults['incoming'] ?? '—']) }}</x-badge>
|
||||
<x-badge tone="neutral">{{ __('servers.firewall_outgoing', ['value' => $fwDefaults['outgoing'] ?? '—']) }}</x-badge>
|
||||
@else
|
||||
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_zone') }}</span>
|
||||
<x-badge tone="neutral">{{ $fwDefaults['zone'] ?? '—' }}</x-badge>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Lone firewall: defaults/state in the left column, rules list in the
|
||||
right. Two-tool case keeps the single stacked column. --}}
|
||||
<div @class(['lg:grid lg:grid-cols-2 lg:divide-x lg:divide-line' => $lone])>
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2 border-b border-line px-4 py-2.5 sm:px-5">
|
||||
@if ($fwTool === 'ufw')
|
||||
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_default') }}</span>
|
||||
<x-badge tone="neutral">{{ __('servers.firewall_incoming', ['value' => $fwDefaults['incoming'] ?? '—']) }}</x-badge>
|
||||
<x-badge tone="neutral">{{ __('servers.firewall_outgoing', ['value' => $fwDefaults['outgoing'] ?? '—']) }}</x-badge>
|
||||
@else
|
||||
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.firewall_zone') }}</span>
|
||||
<x-badge tone="neutral">{{ $fwDefaults['zone'] ?? '—' }}</x-badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($fwReadOnly)
|
||||
<div class="flex items-start gap-2.5 border-b border-line bg-inset px-4 py-2.5 sm:px-5">
|
||||
<x-icon name="lock" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-ink-4" />
|
||||
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ __('servers.firewall_readonly') }}</p>
|
||||
@if ($fwReadOnly)
|
||||
<div class="flex items-start gap-2.5 border-b border-line bg-inset px-4 py-2.5 sm:px-5">
|
||||
<x-icon name="lock" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-ink-4" />
|
||||
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ __('servers.firewall_readonly') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="divide-y divide-line">
|
||||
@forelse ($fwRules as $rule)
|
||||
<div class="divide-y divide-line">
|
||||
@forelse ($fwRules as $rule)
|
||||
{{-- $ruleTone, NOT $tone: the page-level $tone is a closure used by the
|
||||
gauges/Volumes panel; reusing the name here would clobber it. --}}
|
||||
@php $ruleTone = $actTone[$rule['action'] ?? 'ALLOW'] ?? 'online'; @endphp
|
||||
|
|
@ -362,11 +373,12 @@
|
|||
</x-btn>
|
||||
@endunless
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-4 py-4 sm:px-5">
|
||||
<p class="font-mono text-[11px] text-ink-3">{{ __('servers.firewall_no_rules') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
@empty
|
||||
<div class="px-4 py-4 sm:px-5">
|
||||
<p class="font-mono text-[11px] text-ink-3">{{ __('servers.firewall_no_rules') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-panel>
|
||||
|
|
@ -404,39 +416,52 @@
|
|||
</x-btn>
|
||||
</div>
|
||||
@else
|
||||
{{-- Compact summary; the full jail/banned-IP list (which can grow very
|
||||
long) lives in a scrollable modal, opened via the button below. --}}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-4 py-3 sm:px-5">
|
||||
<p class="font-mono text-[11px] text-ink-3">
|
||||
<span class="text-online">{{ __('common.active') }}</span>
|
||||
· <span @class(['text-offline' => $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }}</span>
|
||||
· {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}
|
||||
</p>
|
||||
<x-btn variant="secondary" size="sm" class="shrink-0"
|
||||
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-bans', arguments: { serverId: {{ $server->id }} } })">
|
||||
<x-icon name="lock" class="h-3.5 w-3.5" /> {{ __('servers.fail2ban_view_bans') }}
|
||||
</x-btn>
|
||||
</div>
|
||||
|
||||
{{-- Whitelist (ignoreip) --}}
|
||||
<div class="border-t border-line px-4 py-3 sm:px-5">
|
||||
<p class="mb-2 font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.whitelist_label') }}</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach ($f2bIgnore as $ip)
|
||||
<span class="inline-flex items-center gap-1.5 rounded-sm border border-line bg-inset px-2 py-1 font-mono text-[11px] text-ink-2">
|
||||
{{ $ip }}
|
||||
@unless (in_array($ip, ['127.0.0.1/8', '::1'], true))
|
||||
<button type="button" wire:click="removeIgnore({{ \Illuminate\Support\Js::from($ip) }})" class="text-ink-4 hover:text-offline" title="{{ __('common.remove') }}">
|
||||
<x-icon name="x" class="h-3 w-3" />
|
||||
</button>
|
||||
@endunless
|
||||
</span>
|
||||
@endforeach
|
||||
{{-- Lone fail2ban: status summary + "Gesperrte IPs ansehen" in the left
|
||||
column, the whitelist (ignoreip) management in the right. Two-tool
|
||||
case keeps the single stacked column. --}}
|
||||
<div @class(['lg:grid lg:grid-cols-2 lg:divide-x lg:divide-line' => $lone])>
|
||||
{{-- Compact summary; the full jail/banned-IP list (which can grow very
|
||||
long) lives in a scrollable modal, opened via the button below. --}}
|
||||
<div @class([
|
||||
'flex flex-wrap items-center justify-between gap-3 px-4 py-3 sm:px-5',
|
||||
'border-b border-line lg:border-b-0' => $lone,
|
||||
'border-b border-line' => ! $lone,
|
||||
])>
|
||||
<p class="font-mono text-[11px] text-ink-3">
|
||||
<span class="text-online">{{ __('common.active') }}</span>
|
||||
· <span @class(['text-offline' => $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }}</span>
|
||||
· {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}
|
||||
</p>
|
||||
<x-btn variant="secondary" size="sm" class="shrink-0"
|
||||
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-bans', arguments: { serverId: {{ $server->id }} } })">
|
||||
<x-icon name="lock" class="h-3.5 w-3.5" /> {{ __('servers.fail2ban_view_bans') }}
|
||||
</x-btn>
|
||||
</div>
|
||||
<div class="mt-2.5 flex items-center gap-2">
|
||||
<input wire:model="newIgnoreIp" wire:keydown.enter="addIgnore" type="text" placeholder="{{ __('servers.whitelist_placeholder') }}"
|
||||
class="h-8 w-full max-w-xs rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
|
||||
<x-btn variant="secondary" size="sm" wire:click="addIgnore">{{ __('common.add') }}</x-btn>
|
||||
|
||||
{{-- Whitelist (ignoreip) --}}
|
||||
<div @class([
|
||||
'px-4 py-3 sm:px-5',
|
||||
'border-t border-line lg:border-t-0' => $lone,
|
||||
'border-t border-line' => ! $lone,
|
||||
])>
|
||||
<p class="mb-2 font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('servers.whitelist_label') }}</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach ($f2bIgnore as $ip)
|
||||
<span class="inline-flex items-center gap-1.5 rounded-sm border border-line bg-inset px-2 py-1 font-mono text-[11px] text-ink-2">
|
||||
{{ $ip }}
|
||||
@unless (in_array($ip, ['127.0.0.1/8', '::1'], true))
|
||||
<button type="button" wire:click="removeIgnore({{ \Illuminate\Support\Js::from($ip) }})" class="text-ink-4 hover:text-offline" title="{{ __('common.remove') }}">
|
||||
<x-icon name="x" class="h-3 w-3" />
|
||||
</button>
|
||||
@endunless
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-2.5 flex items-center gap-2">
|
||||
<input wire:model="newIgnoreIp" wire:keydown.enter="addIgnore" type="text" placeholder="{{ __('servers.whitelist_placeholder') }}"
|
||||
class="h-8 w-full max-w-xs rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
|
||||
<x-btn variant="secondary" size="sm" wire:click="addIgnore">{{ __('common.add') }}</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
Loading…
Reference in New Issue