feat: Networks-Members für bridge/bond + System-Rules-Card + Theme-Revert
* Migration 0011: members JSONB für network_interfaces. Bridge/bond brauchen ≥1 Member (NOT VALID-Constraint, schont bestehende Rows). vlan/wireguard/ethernet ignorieren das Feld. * Backend-Validation pro Typ: vlan→parent+vlan_id, bridge/bond→members, ethernet/wireguard→keins. Repo serialisiert via JSONB. * Form Networks: Members-Multi-Select für bridge/bond, Composition- Spalte zeigt vlan-tag bzw. Member-Liste. * Firewall-Rules-Tab zeigt jetzt SystemRulesCard ganz oben — Anti- Lockout (SSH/443), stateful baseline, default-deny-Erklärung. * Theme-Tokens 1:1 mail-gateway: fontSize 13, controlHeight 34 (vorher zu dichtes 12/28). Density kommt vom DataTable size="small". * Makefile publish-amd64 lädt jetzt auch edgeguard-ui_*_all.deb und edgeguard_*_all.deb hoch (vorher nur api). * Version 1.0.0 → 1.0.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ interface NetworkInterface {
|
||||
type: 'ethernet' | 'vlan' | 'bond' | 'bridge' | 'wireguard'
|
||||
parent?: string | null
|
||||
vlan_id?: number | null
|
||||
members: string[]
|
||||
role: 'wan' | 'lan' | 'dmz' | 'mgmt' | 'cluster'
|
||||
mtu?: number | null
|
||||
active: boolean
|
||||
@@ -26,6 +27,7 @@ interface IfaceFormValues {
|
||||
type: NetworkInterface['type']
|
||||
parent?: string
|
||||
vlan_id?: number
|
||||
members?: string[]
|
||||
role: NetworkInterface['role']
|
||||
mtu?: number
|
||||
active: boolean
|
||||
@@ -92,8 +94,14 @@ export default function NetworksPage() {
|
||||
{ title: t('networks.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> },
|
||||
{ title: t('networks.type'), dataIndex: 'type', key: 'type' },
|
||||
{
|
||||
title: t('networks.vlan'), key: 'vlan',
|
||||
render: (_, row) => row.type === 'vlan' ? <span>{row.parent}.{row.vlan_id}</span> : '—',
|
||||
title: t('networks.composition'), key: 'composition',
|
||||
render: (_, row) => {
|
||||
if (row.type === 'vlan') return <span><code>{row.parent}</code>.{row.vlan_id}</span>
|
||||
if (row.type === 'bridge' || row.type === 'bond') {
|
||||
return <Space size={4} wrap>{(row.members ?? []).map((m) => <Tag key={m}>{m}</Tag>)}</Space>
|
||||
}
|
||||
return '—'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('networks.role'), dataIndex: 'role', key: 'role',
|
||||
@@ -109,7 +117,8 @@ export default function NetworksPage() {
|
||||
setEditing(row)
|
||||
form.setFieldsValue({
|
||||
name: row.name, type: row.type, parent: row.parent ?? undefined,
|
||||
vlan_id: row.vlan_id ?? undefined, role: row.role,
|
||||
vlan_id: row.vlan_id ?? undefined, members: row.members ?? [],
|
||||
role: row.role,
|
||||
mtu: row.mtu ?? undefined, active: row.active,
|
||||
description: row.description ?? undefined,
|
||||
})
|
||||
@@ -183,22 +192,42 @@ export default function NetworksPage() {
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(p, c) => p.type !== c.type}>
|
||||
{({ getFieldValue }) => getFieldValue('type') === 'vlan' ? (
|
||||
<>
|
||||
<Form.Item label={t('networks.parent')} name="parent" rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder={t('networks.selectParent')}
|
||||
showSearch
|
||||
options={(sys ?? [])
|
||||
.filter((i) => i.ifname !== 'lo')
|
||||
.map((i) => ({ value: i.ifname, label: i.ifname }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('networks.vlanId')} name="vlan_id" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={4094} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
{({ getFieldValue }) => {
|
||||
const tp = getFieldValue('type') as NetworkInterface['type'] | undefined
|
||||
const sysOptions = (sys ?? [])
|
||||
.filter((i) => i.ifname !== 'lo')
|
||||
.map((i) => ({ value: i.ifname, label: i.ifname }))
|
||||
if (tp === 'vlan') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item label={t('networks.parent')} name="parent" rules={[{ required: true }]}>
|
||||
<Select placeholder={t('networks.selectParent')} showSearch options={sysOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('networks.vlanId')} name="vlan_id" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={4094} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (tp === 'bridge' || tp === 'bond') {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('networks.members')}
|
||||
name="members"
|
||||
rules={[{ required: true, type: 'array', min: 1, message: t('networks.membersRequired') }]}
|
||||
extra={tp === 'bridge' ? t('networks.membersHintBridge') : t('networks.membersHintBond')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t('networks.selectMembers')}
|
||||
showSearch
|
||||
options={sysOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item label={t('networks.role')} name="role" rules={[{ required: true }]}>
|
||||
<Select options={(['wan','lan','dmz','mgmt','cluster'] as const).map(r => ({ value: r, label: r.toUpperCase() }))} />
|
||||
|
||||
Reference in New Issue
Block a user