- 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>
132 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|