feat(firewall): Regeln nach Zone-Pair gruppieren — v1.2.65

- Regeln werden standardmäßig nach Zone-Pair (src→dst) gruppiert
- Jede Gruppe hat einen farbigen Header mit Zone-Badges + Regelanzahl
- Toggle-Button in der Filter-Bar: Gruppen-Ansicht ↔ flache Liste
- Move-up/down bleibt global korrekt (Priority über alle Gruppen)
- CSS: .fw-zone-section* mit nahtlosem Header → Tabelle Übergang
- i18n EN + DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-02 14:04:56 +02:00
parent 9580070a50
commit d425b696f1
5 changed files with 125 additions and 26 deletions

View File

@@ -7,8 +7,8 @@ import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import {
ArrowDownOutlined, ArrowUpOutlined, CopyOutlined, DeleteOutlined, EditOutlined,
EyeOutlined, FireOutlined, PlusOutlined, WarningOutlined,
AppstoreOutlined, ArrowDownOutlined, ArrowUpOutlined, CopyOutlined, DeleteOutlined, EditOutlined,
EyeOutlined, FireOutlined, PlusOutlined, UnorderedListOutlined, WarningOutlined,
} from '@ant-design/icons'
import { InlineNote, InlineLabels } from './InlineEditors'
@@ -189,6 +189,7 @@ export default function RulesTab() {
const [filterAction, setFilterAction] = useState<string>('')
const [filterZone, setFilterZone] = useState<string>('')
const [groupByZone, setGroupByZone] = useState(true)
const [editing, setEditing] = useState<FwRule | null>(null)
const [creating, setCreating] = useState(false)
const [form] = Form.useForm<FormValues>()
@@ -198,6 +199,25 @@ export default function RulesTab() {
[rules],
)
const groupedSections = useMemo(() => {
const map = new Map<string, { srcZone: string; dstZone: string; rules: FwRule[] }>()
for (const r of filteredRules) {
const key = `${r.src_zone}${r.dst_zone}`
if (!map.has(key)) map.set(key, { srcZone: r.src_zone, dstZone: r.dst_zone, rules: [] })
map.get(key)!.rules.push(r)
}
return Array.from(map.entries()).map(([key, v]) => ({ key, ...v }))
}, [filteredRules])
const rowClassFn = (row: FwRule) => {
const c = counterByID.get(row.id)
const zeroHit = row.enabled && (!c || c.packets === 0)
return [
!row.enabled ? 'fw-rule-row--disabled' : '',
zeroHit ? 'fw-rule-row--zero-hit' : '',
].filter(Boolean).join(' ')
}
const filteredRules = useMemo(() => {
let r = sortedRules
if (searchText) {
@@ -518,6 +538,14 @@ export default function RulesTab() {
</Text>
)}
<div className="fw-filter-bar-right">
<Tooltip title={groupByZone ? t('fw.filter.flatView') : t('fw.filter.groupView')}>
<Button
type={groupByZone ? 'default' : 'text'}
size="small"
icon={groupByZone ? <AppstoreOutlined /> : <UnorderedListOutlined />}
onClick={() => setGroupByZone(v => !v)}
/>
</Tooltip>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
{t('fw.rule.add')}
@@ -526,20 +554,8 @@ export default function RulesTab() {
</div>
</div>
<DataTable
rowKey="id"
loading={isLoading}
dataSource={filteredRules}
columns={columns}
rowClassName={(row: FwRule) => {
const c = counterByID.get(row.id)
const zeroHit = row.enabled && (!c || c.packets === 0)
return [
!row.enabled ? 'fw-rule-row--disabled' : '',
zeroHit ? 'fw-rule-row--zero-hit' : '',
].filter(Boolean).join(' ')
}}
emptyContent={
{groupByZone ? (
groupedSections.length === 0 ? (
<EmptyState
icon={<FireOutlined />}
title={activeFilters ? t('fw.filter.noResults') : t('fw.rule.emptyTitle')}
@@ -554,8 +570,53 @@ export default function RulesTab() {
) : undefined
}
/>
}
/>
) : (
groupedSections.map(({ key, srcZone, dstZone, rules: groupRules }) => (
<div key={key} className="fw-zone-section">
<div className="fw-zone-section-header">
<ZoneBadge zone={srcZone} />
<span className="fw-zone-section-arrow"></span>
<ZoneBadge zone={dstZone} />
<span className="fw-zone-section-count">
{groupRules.length} {t('fw.filter.rules')}
</span>
</div>
<DataTable
rowKey="id"
loading={isLoading}
dataSource={groupRules}
columns={columns}
rowClassName={rowClassFn}
pagination={false}
/>
</div>
))
)
) : (
<DataTable
rowKey="id"
loading={isLoading}
dataSource={filteredRules}
columns={columns}
rowClassName={rowClassFn}
emptyContent={
<EmptyState
icon={<FireOutlined />}
title={activeFilters ? t('fw.filter.noResults') : t('fw.rule.emptyTitle')}
description={activeFilters ? t('fw.filter.noResultsHint') : t('fw.rule.emptyDesc')}
action={
!activeFilters ? (
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
{t('fw.rule.add')}
</Button>
</Tooltip>
) : undefined
}
/>
}
/>
)}
<Modal
title={editing ? t('fw.rule.edit') : t('fw.rule.add')}