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
co-authored by Claude Sonnet 4.6
parent 13cb9a8fc4
commit b48ba65ce3
11 changed files with 425 additions and 33 deletions
+34 -7
View File
@@ -10,6 +10,7 @@ import {
ArrowDownOutlined, ArrowUpOutlined, CopyOutlined, DeleteOutlined, EditOutlined,
EyeOutlined, FireOutlined, PlusOutlined, WarningOutlined,
} from '@ant-design/icons'
import { InlineNote, InlineLabels } from './InlineEditors'
const { Text } = Typography
@@ -251,6 +252,22 @@ export default function RulesTab() {
onError: (e: Error) => message.error(e.message),
})
const patchNote = useMutation({
mutationFn: async ({ id, note }: { id: number; note: string }) => {
await apiClient.patch(`/firewall/rules/${id}`, { note })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) },
onError: (e: Error) => message.error(e.message),
})
const patchLabels = useMutation({
mutationFn: async ({ id, labels }: { id: number; labels: string[] }) => {
await apiClient.patch(`/firewall/rules/${id}`, { labels })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) },
onError: (e: Error) => message.error(e.message),
})
const duplicate = useMutation({
mutationFn: async (r: FwRule) => {
const { id: _id, created_at: _ca, updated_at: _ua, ...rest } = r as FwRule & { created_at?: unknown; updated_at?: unknown }
@@ -337,14 +354,24 @@ export default function RulesTab() {
title: t('fw.rule.name'), key: 'name', ellipsis: true,
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>
}
<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: 1 }}>{r.comment}</div>
? <div style={{ fontSize: 11, color: '#64748B', marginTop: 2 }}>{r.comment}</div>
: <div className="fw-rule-desc">{autoDescription(r)}</div>
}
</div>