From 13cb9a8fc42e349731e11e27c82d534838c75556 Mon Sep 17 00:00:00 2001 From: Debian Date: Sun, 31 May 2026 18:27:28 +0200 Subject: [PATCH] feat(firewall): NAT-Regeln enterprise-Design + Duplicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status-Dot, monospace Priorität, icon-only Hover-Actions - Duplicate-Button (CopyOutlined) — disabled=false copy + priority+1 - rowClassName für deaktivierte NAT-Regeln (fw-rule-row--disabled) - Spaltenheader bereinigt (kein 'Edit'-Text mehr) Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- management-ui/src/pages/Firewall/NATRules.tsx | 132 ++++++++++++------ 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/VERSION b/VERSION index 3148141..7ffb8be 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.47 +1.2.48 diff --git a/management-ui/src/pages/Firewall/NATRules.tsx b/management-ui/src/pages/Firewall/NATRules.tsx index 43dcc90..6b58ffc 100644 --- a/management-ui/src/pages/Firewall/NATRules.tsx +++ b/management-ui/src/pages/Firewall/NATRules.tsx @@ -1,9 +1,11 @@ import { useMemo, useState } from 'react' -import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Tag, Tooltip, message } from 'antd' +import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Tag, Tooltip, Typography, message } from 'antd' import type { ColumnsType } from 'antd/es/table' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { ArrowDownOutlined, ArrowUpOutlined, BranchesOutlined } from '@ant-design/icons' +import { ArrowDownOutlined, ArrowUpOutlined, BranchesOutlined, CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons' + +const { Text } = Typography import DataTable from '../../components/DataTable' import EmptyState from '../../components/EmptyState' @@ -86,6 +88,23 @@ export default function NATRulesTab() { onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) }, onError: (e: Error) => message.error(e.message), }) + + const duplicate = useMutation({ + mutationFn: async (r: NATRule) => { + const { id: _id, created_at: _ca, updated_at: _ua, ...rest } = r as NATRule & { created_at?: unknown; updated_at?: unknown } + await apiClient.post('/firewall/nat-rules', { + ...rest, + name: r.name ? `${r.name} (copy)` : undefined, + priority: r.priority + 1, + enabled: false, + }) + }, + onSuccess: () => { + message.success(t('fw.rule.duplicated')) + void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) + }, + onError: (e: Error) => message.error(e.message), + }) const quickToggle = useMutation({ mutationFn: async ({ id, row, checked }: { id: number; row: NATRule; checked: boolean }) => { await apiClient.put(`/firewall/nat-rules/${id}`, { ...row, enabled: checked }) @@ -114,35 +133,71 @@ export default function NATRulesTab() { return {r.target_addr}{r.target_port_start ? `:${r.target_port_start}${r.target_port_end !== r.target_port_start ? `-${r.target_port_end}` : ''}` : ''} } + const openEdit = (row: NATRule) => { + setEditing(row) + form.setFieldsValue({ + name: row.name ?? undefined, + priority: row.priority, enabled: row.enabled, kind: row.kind, + in_zone: row.in_zone ?? undefined, out_zone: row.out_zone ?? undefined, + proto: row.proto ?? undefined, + match_src_cidr: row.match_src_cidr ?? undefined, + match_dst_cidr: row.match_dst_cidr ?? undefined, + match_dport_start: row.match_dport_start ?? undefined, + match_dport_end: row.match_dport_end ?? undefined, + target_addr: row.target_addr ?? undefined, + target_port_start: row.target_port_start ?? undefined, + target_port_end: row.target_port_end ?? undefined, + comment: row.comment ?? undefined, + }) + } + const columns: ColumnsType = [ - { title: '#', dataIndex: 'priority', key: 'priority', width: 70 }, - { title: t('fw.nat.kind'), dataIndex: 'kind', key: 'kind', width: 100, render: (k: NATRule['kind']) => {k.toUpperCase()} }, { - title: t('fw.nat.name'), key: 'name', width: 200, + title: '', key: 'dot', width: 28, + render: (_, row) => ( + + + + ), + }, + { + title: '#', dataIndex: 'priority', key: 'priority', width: 52, + render: (v: number) => ( + {v} + ), + }, + { + title: t('fw.nat.kind'), dataIndex: 'kind', key: 'kind', width: 110, + render: (k: NATRule['kind']) => {k.toUpperCase()}, + }, + { + title: t('fw.nat.name'), key: 'name', render: (_, r) => (
- {r.name &&
{r.name}
} - {r.comment &&
{r.comment}
} - {!r.name && !r.comment && } + {r.name + ?
{r.name}
+ :
{t('fw.rule.unnamed')}
+ } + {r.comment &&
{r.comment}
}
), }, { title: t('fw.nat.match'), key: 'match', render: (_, r) => ( - - {r.in_zone && in:{r.in_zone}} - {r.out_zone && out:{r.out_zone}} - {r.proto && {r.proto}} - {r.match_src_cidr && src={r.match_src_cidr}} - {r.match_dst_cidr && dst={r.match_dst_cidr}} - {r.match_dport_start && dport={r.match_dport_start}{r.match_dport_end && r.match_dport_end !== r.match_dport_start ? `-${r.match_dport_end}` : ''}} + + {r.in_zone && in:{r.in_zone}} + {r.out_zone && out:{r.out_zone}} + {r.proto && {r.proto}} + {r.match_src_cidr && src={r.match_src_cidr}} + {r.match_dst_cidr && dst={r.match_dst_cidr}} + {r.match_dport_start && :{r.match_dport_start}{r.match_dport_end && r.match_dport_end !== r.match_dport_start ? `-${r.match_dport_end}` : ''}} ), }, { title: t('fw.nat.target'), key: 'target', width: 200, render: (_, r) => renderTarget(r) }, { - title: t('fw.nat.enabled'), dataIndex: 'enabled', key: 'enabled', width: 80, + title: t('fw.nat.enabled'), dataIndex: 'enabled', key: 'enabled', width: 68, render: (v: boolean, row: NATRule) => ( { const idx = sortedNAT.findIndex(r => r.id === row.id) const swapping = swapNAT.isPending return ( - + - + + + + +