management-ui/src/pages/Firewall/:
* index.tsx — AntD Tabs default=Rules
* AddressObjects.tsx — Table + Modal (kind-Switch ändert Placeholder)
* AddressGroups.tsx — Members als Multi-Select aus Address-Objects
* Services.tsx — Builtin-Rows sind Edit/Delete-disabled mit Tooltip,
Form blendet Port-Felder bei proto != tcp/udp aus
* ServiceGroups.tsx — analog AddressGroups
* Rules.tsx — Renderer mit object/group/cidr/any-Switch pro Seite
+ Service-Picker; Action+Zone als Tags in der Tabelle
* NATRules.tsx — kind-spezifische Form (DNAT braucht in_zone+dport,
SNAT/MASQ braucht out_zone, MASQ verbietet target_addr)
Sidebar bekommt eigene Sektion "Sicherheit" mit FireOutlined-Icon
für /firewall. i18n de/en für alle 6 Tabs + Form-Labels.
Backend war schon im vorigen Commit fertig — diese Pages konsumieren
direkt /api/v1/firewall/{address-objects,address-groups,services,
service-groups,rules,nat-rules}. Renderer (nft aus den Joins) +
auto-apply folgen in den nächsten Commits — bis dahin sind die Rules
in der DB sichtbar aber noch nicht aktiv im Kernel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
import { Suspense, lazy, useEffect, type ReactNode } from 'react'
|
|
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
|
import { ConfigProvider, Spin } from 'antd'
|
|
import deDE from 'antd/locale/de_DE'
|
|
import enUS from 'antd/locale/en_US'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import AppLayout from './components/Layout/AppLayout'
|
|
import apiClient, { isEnvelope } from './api/client'
|
|
import { useAuthStore, type SessionUser } from './stores/auth'
|
|
|
|
const LoginPage = lazy(() => import('./pages/Login'))
|
|
const SetupPage = lazy(() => import('./pages/Setup'))
|
|
const DashboardPage = lazy(() => import('./pages/Dashboard'))
|
|
const DomainsPage = lazy(() => import('./pages/Domains'))
|
|
const BackendsPage = lazy(() => import('./pages/Backends'))
|
|
const RoutingRulesPage = lazy(() => import('./pages/RoutingRules'))
|
|
const NetworksPage = lazy(() => import('./pages/Networks'))
|
|
const IPAddressesPage = lazy(() => import('./pages/IPAddresses'))
|
|
const SSLPage = lazy(() => import('./pages/SSL'))
|
|
const FirewallPage = lazy(() => import('./pages/Firewall'))
|
|
const ClusterPage = lazy(() => import('./pages/Cluster'))
|
|
const SettingsPage = lazy(() => import('./pages/Settings'))
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: 1, refetchOnWindowFocus: false },
|
|
},
|
|
})
|
|
|
|
// Theme tokens 1:1 wie mail-gateway/enconf — colorPrimary, font,
|
|
// borderRadius, controlHeight. enterprise.css ergänzt mit eigenen
|
|
// Layout-Klassen (.app-layout, .sidebar, .header, …).
|
|
const antdTheme = {
|
|
token: {
|
|
colorPrimary: '#0EA5E9',
|
|
borderRadius: 6,
|
|
borderRadiusLG: 8,
|
|
fontSize: 13,
|
|
fontWeightStrong: 600,
|
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
colorBgContainer: '#FFFFFF',
|
|
colorBgLayout: '#F8FAFC',
|
|
colorBorder: '#E2E8F0',
|
|
colorText: '#0F172A',
|
|
colorTextSecondary: '#64748B',
|
|
controlHeight: 34,
|
|
},
|
|
}
|
|
|
|
function RequireAuth({ children }: { children: ReactNode }) {
|
|
const user = useAuthStore((s) => s.user)
|
|
const location = useLocation()
|
|
if (!user) {
|
|
return <Navigate to="/login" replace state={{ from: location }} />
|
|
}
|
|
return <>{children}</>
|
|
}
|
|
|
|
function SetupGate({ children }: { children: ReactNode }) {
|
|
const location = useLocation()
|
|
useEffect(() => {
|
|
if (location.pathname.startsWith('/setup')) return
|
|
apiClient.get('/setup/status').then((r) => {
|
|
const env = r.data
|
|
if (isEnvelope(env)) {
|
|
const data = env.data as { completed?: boolean } | undefined
|
|
if (data && data.completed === false) {
|
|
window.location.replace('/setup')
|
|
}
|
|
}
|
|
}).catch(() => { /* setup-gate is best-effort */ })
|
|
}, [location.pathname])
|
|
return <>{children}</>
|
|
}
|
|
|
|
export default function App() {
|
|
const { i18n } = useTranslation()
|
|
const antdLocale = i18n.language?.startsWith('de') ? deDE : enUS
|
|
|
|
return (
|
|
<ConfigProvider theme={antdTheme} locale={antdLocale}>
|
|
<QueryClientProvider client={queryClient}>
|
|
<BrowserRouter>
|
|
<SetupGate>
|
|
<Suspense fallback={<div className="loader-center"><Spin size="large" /></div>}>
|
|
<Routes>
|
|
<Route path="/setup" element={<SetupPage onComplete={(u: SessionUser) => useAuthStore.getState().set(u)} />} />
|
|
<Route path="/login" element={<LoginPage onLogin={(u: SessionUser) => useAuthStore.getState().set(u)} />} />
|
|
|
|
<Route element={<RequireAuth><AppLayout /></RequireAuth>}>
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
<Route path="/dashboard" element={<DashboardPage />} />
|
|
<Route path="/domains" element={<DomainsPage />} />
|
|
<Route path="/backends" element={<BackendsPage />} />
|
|
<Route path="/routing-rules" element={<RoutingRulesPage />} />
|
|
<Route path="/networks" element={<NetworksPage />} />
|
|
<Route path="/ip-addresses" element={<IPAddressesPage />} />
|
|
<Route path="/ssl" element={<SSLPage />} />
|
|
<Route path="/firewall" element={<FirewallPage />} />
|
|
<Route path="/cluster" element={<ClusterPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
</Route>
|
|
|
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</SetupGate>
|
|
</BrowserRouter>
|
|
</QueryClientProvider>
|
|
</ConfigProvider>
|
|
)
|
|
}
|