feat(ui): Quick-Toggles, Config-Preview, Dashboard-Alerts, Domain-Detail-Health
Quick-Toggle-Switches (kein Modal nötig) für: Backends, Backend-Server, DNS-Zonen, DNS-Records, Domains (active), Firewall-Rules, NAT-Rules, Forward-Proxy ACLs, Routing-Rules. Dashboard: Alert-Banner für komplett ausgefallene Backends (HAProxy-Stats) und Domains im Maintenance-Mode. Domain-Detail: HAProxy-Live-Health-Badge (15s Polling), TLS-Cert ausstellen/erneuern direkt aus dem Detail, Routing-Rules-Panel inline. Config-Preview (Settings): alle 4 Generatoren (haproxy, nftables, squid, unbound) rendern via RenderToString ohne Disk-Write — GET /system/config-preview. ActionButtons: Viewer-Rolle blendet Delete aus (RBAC-Ergänzung). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Button, Popconfirm, Space, Tooltip } from 'antd'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
// ActionButtons is the standard "Edit / Delete" pair used at the
|
||||
// end of every CRUD table row. Centralising it means we only style
|
||||
@@ -8,6 +9,9 @@ import { useTranslation } from 'react-i18next'
|
||||
//
|
||||
// Either prop may be omitted to suppress that button — useful for
|
||||
// rows that aren't editable (e.g. builtin services).
|
||||
// Viewer-role accounts automatically get delete disabled — mutations
|
||||
// are blocked at the API level too, but hiding the button avoids
|
||||
// confusing "access denied" errors for read-only users.
|
||||
interface ActionButtonsProps {
|
||||
onEdit?: () => void
|
||||
onDelete?: () => void
|
||||
@@ -28,6 +32,14 @@ export default function ActionButtons({
|
||||
editDisabledReason, deleteDisabledReason,
|
||||
}: ActionButtonsProps) {
|
||||
const { t } = useTranslation()
|
||||
const role = useAuthStore((s) => s.user?.role)
|
||||
const isViewer = role === 'viewer'
|
||||
|
||||
const delDisabled = deleteDisabled || isViewer
|
||||
const delDisabledReason = isViewer
|
||||
? t('auth.viewerBadge')
|
||||
: deleteDisabledReason
|
||||
|
||||
return (
|
||||
<Space size={4}>
|
||||
{onEdit && (
|
||||
@@ -42,8 +54,8 @@ export default function ActionButtons({
|
||||
</Tooltip>
|
||||
)}
|
||||
{onDelete && (
|
||||
deleteDisabled ? (
|
||||
<Tooltip title={deleteDisabledReason ?? t('common.delete')}>
|
||||
delDisabled ? (
|
||||
<Tooltip title={delDisabledReason ?? t('common.delete')}>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} disabled />
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
||||
@@ -14,7 +14,7 @@ const PAGE_TITLES: Record<string, string> = {
|
||||
'/dashboard': 'nav.dashboard',
|
||||
'/domains': 'nav.domains',
|
||||
'/backends': 'nav.backends',
|
||||
'/routing-rules': 'nav.routing',
|
||||
'/routing-rules': 'nav.routing',
|
||||
'/networks': 'nav.networks',
|
||||
'/ip-addresses': 'nav.ipAddresses',
|
||||
'/firewall/live': 'nav.firewallLive',
|
||||
|
||||
@@ -6,6 +6,7 @@ import apiClient, { isEnvelope } from '../../api/client'
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
BellOutlined,
|
||||
BranchesOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudServerOutlined,
|
||||
ClusterOutlined,
|
||||
@@ -53,8 +54,9 @@ const NAV: NavSection[] = [
|
||||
{
|
||||
labelKey: 'nav.section.routing',
|
||||
items: [
|
||||
{ path: '/domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
|
||||
{ path: '/backends', labelKey: 'nav.backends', icon: <DatabaseOutlined /> },
|
||||
{ path: '/domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
|
||||
{ path: '/backends', labelKey: 'nav.backends', icon: <DatabaseOutlined /> },
|
||||
{ path: '/routing-rules', labelKey: 'nav.routing', icon: <BranchesOutlined /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user