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:
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user