Files
edgeguard-native/management-ui/src/pages/Firewall/index.tsx
Debian e999eb68c2 fix(firewall/ui): Card-Tabs + no-cache Header für index.html
- Tabs type="card": jeder Tab hat eigene sichtbare Box statt Unterstrich
- cardBg: #F1F5F9 für inaktive Card-Tabs
- main.go: index.html + SPA-Fallback mit no-cache Header ausliefern
  (Vite hashed assets /assets/* bleiben immutable=1Jahr cached)
- Verhindert dass der Browser veraltete index.html nach Updates cached

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:37:56 +02:00

132 lines
5.4 KiB
TypeScript

import { Space, Tag, Tabs } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, FireOutlined, SafetyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import apiClient, { isEnvelope } from '../../api/client'
import PageHeader from '../../components/PageHeader'
import AddressObjectsTab from './AddressObjects'
import AddressGroupsTab from './AddressGroups'
import ServicesTab from './Services'
import ServiceGroupsTab from './ServiceGroups'
import RulesTab from './Rules'
import NATRulesTab from './NATRules'
import SystemRulesCard from './SystemRules'
import ZonesTab from './Zones'
import type { FwRule, FwZone, NATRule } from './types'
interface ServiceStatus { label: string; unit: string; active: boolean; state: string }
async function listRulesCount(): Promise<FwRule[]> {
try {
const r = await apiClient.get('/firewall/rules')
if (!isEnvelope(r.data)) return []
return (r.data.data as { rules?: FwRule[] }).rules ?? []
} catch { return [] }
}
async function listNATCount(): Promise<NATRule[]> {
try {
const r = await apiClient.get('/firewall/nat-rules')
if (!isEnvelope(r.data)) return []
return (r.data.data as { nat_rules?: NATRule[] }).nat_rules ?? []
} catch { return [] }
}
async function listZonesCount(): Promise<FwZone[]> {
try {
const r = await apiClient.get('/firewall/zones')
if (!isEnvelope(r.data)) return []
return (r.data.data as { zones?: FwZone[] }).zones ?? []
} catch { return [] }
}
function FirewallKPIStrip({ nftablesActive }: { nftablesActive: boolean | undefined }) {
const { t } = useTranslation()
const { data: rules } = useQuery({ queryKey: ['fw', 'rules'], queryFn: listRulesCount, staleTime: 30_000 })
const { data: natRules } = useQuery({ queryKey: ['fw', 'nat-rules'], queryFn: listNATCount, staleTime: 30_000 })
const { data: zones } = useQuery({ queryKey: ['fw', 'zones'], queryFn: listZonesCount, staleTime: 30_000 })
const totalRules = rules?.length ?? 0
const activeRules = rules?.filter(r => r.enabled).length ?? 0
const totalNAT = natRules?.length ?? 0
const totalZones = zones?.length ?? 0
return (
<div className="fw-kpi-strip">
<div className="fw-kpi-card">
<div className="fw-kpi-label">{t('fw.kpi.policyRules')}</div>
<div className="fw-kpi-value">{totalRules}</div>
<div className="fw-kpi-sub">
{activeRules} {t('fw.kpi.active')} · {totalRules - activeRules} {t('fw.kpi.disabled')}
</div>
</div>
<div className="fw-kpi-card">
<div className="fw-kpi-label">{t('fw.kpi.natRules')}</div>
<div className="fw-kpi-value">{totalNAT}</div>
<div className="fw-kpi-sub">{t('fw.kpi.natHint')}</div>
</div>
<div className="fw-kpi-card">
<div className="fw-kpi-label">{t('fw.kpi.zones')}</div>
<div className="fw-kpi-value">{totalZones}</div>
<div className="fw-kpi-sub">{t('fw.kpi.zonesHint')}</div>
</div>
<div className="fw-kpi-card fw-kpi-card--policy">
<div className="fw-kpi-label">{t('fw.kpi.defaultPolicy')}</div>
<div className="fw-kpi-value--mono">INPUT DROP</div>
<div className="fw-kpi-sub" style={{ marginTop: 4 }}>
{nftablesActive === true && <span style={{ color: '#15803D' }}> {t('fw.kpi.nftActive')}</span>}
{nftablesActive === false && <span style={{ color: '#B91C1C' }}> {t('fw.kpi.nftInactive')}</span>}
{nftablesActive === undefined && <span style={{ color: '#94A3B8' }}>···</span>}
</div>
</div>
</div>
)
}
export default function FirewallPage() {
const { t } = useTranslation()
const { data: services } = useQuery({
queryKey: ['system', 'services'],
queryFn: async () => {
const r = await apiClient.get('/system/services')
return isEnvelope(r.data) ? (r.data.data as { services: ServiceStatus[] }).services : []
},
refetchInterval: 30_000,
})
const nftables = services?.find(s => s.unit === 'nftables.service' || s.unit === 'nftables')
const tabs = [
{ key: 'rules', label: <span><SafetyOutlined /> {t('fw.tabs.rules')}</span>, children: <RulesTab /> },
{ key: 'nat', label: t('fw.tabs.nat'), children: <NATRulesTab /> },
{ key: 'zones', label: t('fw.tabs.zones'), children: <ZonesTab /> },
{ key: 'addrObj', label: t('fw.tabs.addrObj'), children: <AddressObjectsTab /> },
{ key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: <AddressGroupsTab /> },
{ key: 'services', label: t('fw.tabs.services'), children: <ServicesTab /> },
{ key: 'svcGrp', label: t('fw.tabs.svcGrp'), children: <ServiceGroupsTab /> },
{ key: 'system', label: t('fw.tabs.system'), children: <SystemRulesCard /> },
]
return (
<div>
<PageHeader
icon={<FireOutlined />}
title={t('fw.title')}
subtitle={t('fw.intro')}
extra={nftables && (
<Tag
icon={nftables.active ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
color={nftables.active ? 'green' : 'red'}
>
<Space size={4}>
<span>nftables</span>
<span style={{ fontWeight: 400, opacity: 0.85 }}>{nftables.state}</span>
</Space>
</Tag>
)}
/>
<FirewallKPIStrip nftablesActive={nftables?.active} />
<Tabs items={tabs} defaultActiveKey="rules" type="card" />
</div>
)
}