feat(firewall): Move-up/down buttons for rule priority reordering
Adds ↑↓ icon buttons in the firewall rules table. Clicking swaps the rule's priority value with its sorted neighbor via two sequential PUTs. First rule's ↑ and last rule's ↓ are disabled. Viewer role cannot reorder. v1.1.102 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ import (
|
|||||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.101"
|
var version = "1.1.102"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.101"
|
var version = "1.1.102"
|
||||||
|
|
||||||
const usage = `edgeguard-ctl — EdgeGuard CLI
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.101"
|
var version = "1.1.102"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -109,6 +109,8 @@
|
|||||||
"cidr": "CIDR",
|
"cidr": "CIDR",
|
||||||
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
|
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
|
||||||
"kinds": { "any": "Beliebig", "object": "Objekt", "group": "Gruppe", "cidr": "CIDR" },
|
"kinds": { "any": "Beliebig", "object": "Objekt", "group": "Gruppe", "cidr": "CIDR" },
|
||||||
|
"moveUp": "Nach oben (höhere Priorität)",
|
||||||
|
"moveDown": "Nach unten (niedrigere Priorität)",
|
||||||
"emptyTitle": "Noch keine eigenen Firewall-Regeln.",
|
"emptyTitle": "Noch keine eigenen Firewall-Regeln.",
|
||||||
"emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen."
|
"emptyDesc": "Die System-Regeln oben halten SSH (rate-limited), HTTPS :443 und Mgmt-UI :3443 immer offen (Anti-Lockout). Eigene Regeln für app-spezifische Inbound-Ports oder zonenübergreifende Forwards anlegen."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,6 +109,8 @@
|
|||||||
"cidr": "CIDR",
|
"cidr": "CIDR",
|
||||||
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
|
"actions": { "accept": "ACCEPT", "drop": "DROP", "reject": "REJECT" },
|
||||||
"kinds": { "any": "Any", "object": "Object", "group": "Group", "cidr": "CIDR" },
|
"kinds": { "any": "Any", "object": "Object", "group": "Group", "cidr": "CIDR" },
|
||||||
|
"moveUp": "Move up (higher priority)",
|
||||||
|
"moveDown": "Move down (lower priority)",
|
||||||
"emptyTitle": "No custom firewall rules yet.",
|
"emptyTitle": "No custom firewall rules yet.",
|
||||||
"emptyDesc": "The system rules above keep SSH (rate-limited), HTTPS :443 and the mgmt UI :3443 open (anti-lockout). Add custom rules for app-specific inbound ports or cross-zone forwards."
|
"emptyDesc": "The system rules above keep SSH (rate-limited), HTTPS :443 and the mgmt UI :3443 open (anti-lockout). Add custom rules for app-specific inbound ports or cross-zone forwards."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Tag, Tooltip, Typography, 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 type { ColumnsType } from 'antd/es/table'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FireOutlined } from '@ant-design/icons'
|
import { ArrowDownOutlined, ArrowUpOutlined, FireOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@@ -183,6 +183,20 @@ export default function RulesTab() {
|
|||||||
onError: (e: Error) => message.error(e.message),
|
onError: (e: Error) => message.error(e.message),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sortedRules = useMemo(
|
||||||
|
() => [...(rules ?? [])].sort((a, b) => a.priority - b.priority),
|
||||||
|
[rules],
|
||||||
|
)
|
||||||
|
|
||||||
|
const swap = useMutation({
|
||||||
|
mutationFn: async ({ a, b }: { a: FwRule; b: FwRule }) => {
|
||||||
|
await apiClient.put(`/firewall/rules/${a.id}`, { ...a, priority: b.priority })
|
||||||
|
await apiClient.put(`/firewall/rules/${b.id}`, { ...b, priority: a.priority })
|
||||||
|
},
|
||||||
|
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['fw', 'rules'] }) },
|
||||||
|
onError: (e: Error) => message.error(e.message),
|
||||||
|
})
|
||||||
|
|
||||||
const editFromRow = (r: FwRule) => {
|
const editFromRow = (r: FwRule) => {
|
||||||
setEditing(r)
|
setEditing(r)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
@@ -246,6 +260,33 @@ export default function RulesTab() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '', key: 'move', width: 64,
|
||||||
|
render: (_, row) => {
|
||||||
|
const idx = sortedRules.findIndex(r => r.id === row.id)
|
||||||
|
const swapping = swap.isPending
|
||||||
|
return (
|
||||||
|
<Space size={2}>
|
||||||
|
<Tooltip title={t('fw.rule.moveUp')}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ArrowUpOutlined />}
|
||||||
|
disabled={isViewer || idx <= 0 || swapping}
|
||||||
|
onClick={() => swap.mutate({ a: row, b: sortedRules[idx - 1] })}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t('fw.rule.moveDown')}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ArrowDownOutlined />}
|
||||||
|
disabled={isViewer || idx >= sortedRules.length - 1 || swapping}
|
||||||
|
onClick={() => swap.mutate({ a: row, b: sortedRules[idx + 1] })}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('common.edit'), key: 'actions',
|
title: t('common.edit'), key: 'actions',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user