feat(firewall): Inline-Note + Labels direkt in der Tabelle

- Migration 0035: note TEXT + labels TEXT[] für firewall_rules + nat_rules
- PATCH /firewall/rules/:id + /nat-rules/:id für note/labels Updates
- InlineNote: gold Tag mit MessageOutlined, Klick zum Bearbeiten (Enter/Blur speichert)
- InlineLabels: geekblue Tags mit X-Button zum Entfernen, "+" zum Hinzufügen
- Name-Spalte: Name + Labels + Note in Zeile 1, comment/auto-desc in Zeile 2
- Gleiche UX für NAT-Regeln

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:46:54 +02:00
parent 13cb9a8fc4
commit b48ba65ce3
11 changed files with 425 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { ArrowDownOutlined, ArrowUpOutlined, BranchesOutlined, CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { InlineNote, InlineLabels } from './InlineEditors'
const { Text } = Typography
@@ -89,6 +90,22 @@ export default function NATRulesTab() {
onError: (e: Error) => message.error(e.message),
})
const patchNote = useMutation({
mutationFn: async ({ id, note }: { id: number; note: string }) => {
await apiClient.patch(`/firewall/nat-rules/${id}`, { note })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'nat'] }) },
onError: (e: Error) => message.error(e.message),
})
const patchLabels = useMutation({
mutationFn: async ({ id, labels }: { id: number; labels: string[] }) => {
await apiClient.patch(`/firewall/nat-rules/${id}`, { labels })
},
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 }
@@ -174,11 +191,23 @@ export default function NATRulesTab() {
title: t('fw.nat.name'), key: 'name',
render: (_, r) => (
<div>
{r.name
? <div className="fw-rule-name" style={{ fontWeight: 500, fontSize: 12, color: '#0F172A' }}>{r.name}</div>
: <div className="fw-rule-name" style={{ fontSize: 12, color: '#94A3B8', fontStyle: 'italic' }}>{t('fw.rule.unnamed')}</div>
}
{r.comment && <div style={{ fontSize: 11, color: '#64748B', marginTop: 1 }}>{r.comment}</div>}
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
{r.name
? <span className="fw-rule-name" style={{ fontWeight: 500, fontSize: 12, color: '#0F172A' }}>{r.name}</span>
: <span className="fw-rule-name" style={{ fontSize: 12, color: '#94A3B8', fontStyle: 'italic' }}>{t('fw.rule.unnamed')}</span>
}
<InlineLabels
labels={r.labels ?? []}
disabled={isViewer}
onSave={labels => patchLabels.mutate({ id: r.id, labels })}
/>
<InlineNote
value={r.note}
disabled={isViewer}
onSave={note => patchNote.mutate({ id: r.id, note })}
/>
</div>
{r.comment && <div style={{ fontSize: 11, color: '#64748B', marginTop: 2 }}>{r.comment}</div>}
</div>
),
},