From d425b696f147790e0f3b7662231e8e6381180be0 Mon Sep 17 00:00:00 2001 From: Debian Date: Tue, 2 Jun 2026 14:04:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(firewall):=20Regeln=20nach=20Zone-Pair=20g?= =?UTF-8?q?ruppieren=20=E2=80=94=20v1.2.65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- VERSION | 2 +- management-ui/src/i18n/locales/de/common.json | 8 +- management-ui/src/i18n/locales/en/common.json | 10 +- management-ui/src/pages/Firewall/Rules.tsx | 97 +++++++++++++++---- management-ui/src/styles/enterprise.css | 34 +++++++ 5 files changed, 125 insertions(+), 26 deletions(-) diff --git a/VERSION b/VERSION index 6efdfbb..5313c2e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.64 +1.2.65 diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 00a741a..08e7f09 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -154,7 +154,6 @@ "emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen.", "logEnabled": "Logging aktiv — gematchte Pakete werden ins Firewall-Log geschrieben", "ruleDisabled": "Regel deaktiviert", - "enabled": "Aktiv", "unnamed": "(kein Name)", "zeroHitHint": "Keine Treffer seit dem letzten Neustart — möglicherweise ungenutzte oder überlagerte Regel" }, @@ -175,7 +174,10 @@ "allActions": "Alle Aktionen", "allZones": "Alle Zonen", "noResults": "Keine Regeln entsprechen dem Filter", - "noResultsHint": "Filter zurücksetzen um alle Regeln zu sehen." + "noResultsHint": "Filter zurücksetzen um alle Regeln zu sehen.", + "groupView": "Nach Zone gruppieren", + "flatView": "Flache Liste", + "rules": "Regeln" }, "nat": { "name": "Name", @@ -1758,4 +1760,4 @@ "confirmRemove": "Collection {{name}} wirklich entfernen?" } } -} +} \ No newline at end of file diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index 9a8008e..dba6886 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -113,7 +113,7 @@ "rule": { "name": "Name", "priority": "Priority", - "enabled": "Enabled", + "enabled": "Active", "log": "Log", "action": "Action", "src": "Source", @@ -154,7 +154,6 @@ "emptyDesc": "The system rules above keep SSH (rate-limited), HTTPS :443 and the mgmt UI :3443 open (anti-lockout). Add custom rules for app-specific inbound ports or cross-zone forwards.", "logEnabled": "Logging active — matched packets are written to the firewall log", "ruleDisabled": "Rule disabled", - "enabled": "Active", "unnamed": "(unnamed)", "zeroHitHint": "No hits since last restart — possibly unused or shadowed rule" }, @@ -175,7 +174,10 @@ "allActions": "All actions", "allZones": "All zones", "noResults": "No rules match the filter", - "noResultsHint": "Clear the filter to see all rules." + "noResultsHint": "Clear the filter to see all rules.", + "groupView": "Group by zone", + "flatView": "Flat list", + "rules": "rules" }, "nat": { "name": "Name", @@ -1758,4 +1760,4 @@ "confirmRemove": "Really remove collection {{name}}?" } } -} +} \ No newline at end of file diff --git a/management-ui/src/pages/Firewall/Rules.tsx b/management-ui/src/pages/Firewall/Rules.tsx index d4b77cf..5c9a59a 100644 --- a/management-ui/src/pages/Firewall/Rules.tsx +++ b/management-ui/src/pages/Firewall/Rules.tsx @@ -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('') const [filterZone, setFilterZone] = useState('') + const [groupByZone, setGroupByZone] = useState(true) const [editing, setEditing] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() @@ -198,6 +199,25 @@ export default function RulesTab() { [rules], ) + const groupedSections = useMemo(() => { + const map = new Map() + 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() { )}
+ +
- { - 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 ? ( } 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 }) => ( +
+
+ + + + + {groupRules.length} {t('fw.filter.rules')} + +
+ +
+ )) + ) + ) : ( + } + title={activeFilters ? t('fw.filter.noResults') : t('fw.rule.emptyTitle')} + description={activeFilters ? t('fw.filter.noResultsHint') : t('fw.rule.emptyDesc')} + action={ + !activeFilters ? ( + + + + ) : undefined + } + /> + } + /> + )}