fix(dns): implement DNS routes with navigation and localization support
This commit is contained in:
+13
-2
@@ -12,6 +12,7 @@ import { AuthProvider } from './context/AuthContext'
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
||||
const ProxyHosts = lazy(() => import('./pages/ProxyHosts'))
|
||||
const RemoteServers = lazy(() => import('./pages/RemoteServers'))
|
||||
const DNS = lazy(() => import('./pages/DNS'))
|
||||
const ImportCaddy = lazy(() => import('./pages/ImportCaddy'))
|
||||
const ImportCrowdSec = lazy(() => import('./pages/ImportCrowdSec'))
|
||||
const Certificates = lazy(() => import('./pages/Certificates'))
|
||||
@@ -63,7 +64,17 @@ export default function App() {
|
||||
<Route path="remote-servers" element={<RemoteServers />} />
|
||||
<Route path="domains" element={<Domains />} />
|
||||
<Route path="certificates" element={<Certificates />} />
|
||||
<Route path="dns-providers" element={<DNSProviders />} />
|
||||
|
||||
{/* DNS Routes */}
|
||||
<Route path="dns" element={<DNS />}>
|
||||
<Route index element={<Navigate to="/dns/providers" replace />} />
|
||||
<Route path="providers" element={<DNSProviders />} />
|
||||
<Route path="plugins" element={<Plugins />} />
|
||||
</Route>
|
||||
|
||||
{/* Legacy redirect for old bookmarks */}
|
||||
<Route path="dns-providers" element={<Navigate to="/dns/providers" replace />} />
|
||||
|
||||
<Route path="security" element={<Security />} />
|
||||
<Route path="security/audit-logs" element={<AuditLogs />} />
|
||||
<Route path="security/access-lists" element={<AccessLists />} />
|
||||
@@ -75,7 +86,7 @@ export default function App() {
|
||||
<Route path="access-lists" element={<AccessLists />} />
|
||||
<Route path="uptime" element={<Uptime />} />
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="admin/plugins" element={<Plugins />} />
|
||||
<Route path="admin/plugins" element={<Navigate to="/dns/plugins" replace />} />
|
||||
<Route path="import" element={<Navigate to="/tasks/import/caddyfile" replace />} />
|
||||
|
||||
{/* Settings Routes */}
|
||||
|
||||
@@ -63,7 +63,10 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{ name: t('navigation.remoteServers'), path: '/remote-servers', icon: '🖥️' },
|
||||
{ name: t('navigation.domains'), path: '/domains', icon: '🌍' },
|
||||
{ name: t('navigation.certificates'), path: '/certificates', icon: '🔒' },
|
||||
{ name: t('navigation.dnsProviders'), path: '/dns-providers', icon: '☁️' },
|
||||
{ name: t('navigation.dns'), path: '/dns', icon: '☁️', children: [
|
||||
{ name: t('navigation.dnsProviders'), path: '/dns/providers', icon: '🧭' },
|
||||
{ name: t('navigation.plugins'), path: '/dns/plugins', icon: '🔌' },
|
||||
] },
|
||||
{ name: t('navigation.uptime'), path: '/uptime', icon: '📈' },
|
||||
{ name: t('navigation.security'), path: '/security', icon: '🛡️', children: [
|
||||
{ name: t('navigation.dashboard'), path: '/security', icon: '🛡️' },
|
||||
@@ -86,14 +89,6 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{ name: t('navigation.accountManagement'), path: '/settings/account-management', icon: '👥' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: t('navigation.admin'),
|
||||
path: '/admin',
|
||||
icon: '👑',
|
||||
children: [
|
||||
{ name: t('navigation.plugins'), path: '/admin/plugins', icon: '🔌' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: t('navigation.tasks'),
|
||||
path: '/tasks',
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"remoteServers": "Remote-Server",
|
||||
"domains": "Domänen",
|
||||
"certificates": "Zertifikate",
|
||||
"dns": "DNS",
|
||||
"dnsProviders": "DNS-Anbieter",
|
||||
"plugins": "Plugins",
|
||||
"security": "Sicherheit",
|
||||
"accessLists": "Zugriffslisten",
|
||||
"crowdsec": "CrowdSec",
|
||||
@@ -974,5 +977,9 @@
|
||||
"strict": "Starke Sicherheit für Web-Anwendungen.\n✓ Ideal für: Web-only Dashboards, Admin-Panels.\n⚠ Kann mobile Apps und API-Clients beeinträchtigen.\nNicht empfohlen für Radarr, Plex oder Dienste mit Companion-Apps.",
|
||||
"paranoid": "Maximale Sicherheit für Hochrisiko-Anwendungen.\n✓ Ideal für: Banking, Gesundheitswesen, Compliance-kritische Apps.\n⚠ WIRD mobile Apps, API-Clients und OAuth-Flows beeinträchtigen.\nNur verwenden, wenn Sie jeden Header verstehen und anpassen können."
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS-Verwaltung",
|
||||
"description": "DNS-Anbieter und Plugins für die Zertifikatsautomatisierung verwalten"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"remoteServers": "Remote Servers",
|
||||
"domains": "Domains",
|
||||
"certificates": "Certificates",
|
||||
"dns": "DNS",
|
||||
"dnsProviders": "DNS Providers",
|
||||
"security": "Security",
|
||||
"accessLists": "Access Lists",
|
||||
@@ -1027,6 +1028,10 @@
|
||||
"paranoid": "Maximum security for high-risk applications.\n✓ Best for: Banking, healthcare, compliance-critical apps.\n⚠ WILL break mobile apps, API clients, and OAuth flows.\nOnly use if you understand and can customize every header."
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS Management",
|
||||
"description": "Manage DNS providers and plugins for certificate automation"
|
||||
},
|
||||
"dnsProviders": {
|
||||
"title": "DNS Providers",
|
||||
"description": "Manage DNS providers for wildcard certificate validation",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"remoteServers": "Servidores Remotos",
|
||||
"domains": "Dominios",
|
||||
"certificates": "Certificados",
|
||||
"dns": "DNS",
|
||||
"dnsProviders": "Proveedores DNS",
|
||||
"plugins": "Plugins",
|
||||
"security": "Seguridad",
|
||||
"accessLists": "Listas de Acceso",
|
||||
"crowdsec": "CrowdSec",
|
||||
@@ -974,5 +977,9 @@
|
||||
"strict": "Seguridad fuerte para aplicaciones web.\n✓ Ideal para: Dashboards solo web, paneles de administración.\n⚠ Puede afectar apps móviles y clientes API.\nNo recomendado para Radarr, Plex o servicios con apps companion.",
|
||||
"paranoid": "Seguridad máxima para aplicaciones de alto riesgo.\n✓ Ideal para: Banca, salud, apps críticas de cumplimiento.\n⚠ AFECTARÁ apps móviles, clientes API y flujos OAuth.\nSolo úselo si entiende y puede personalizar cada cabecera."
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "Gestión DNS",
|
||||
"description": "Administrar proveedores DNS y plugins para la automatización de certificados"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"remoteServers": "Serveurs Distants",
|
||||
"domains": "Domaines",
|
||||
"certificates": "Certificats",
|
||||
"dns": "DNS",
|
||||
"dnsProviders": "Fournisseurs DNS",
|
||||
"plugins": "Plugins",
|
||||
"security": "Sécurité",
|
||||
"accessLists": "Listes d'Accès",
|
||||
"crowdsec": "CrowdSec",
|
||||
@@ -974,5 +977,9 @@
|
||||
"strict": "Sécurité renforcée pour les applications web.\n✓ Idéal pour : Tableaux de bord web uniquement, panneaux d'administration.\n⚠ Peut affecter les apps mobiles et clients API.\nNon recommandé pour Radarr, Plex ou services avec apps companion.",
|
||||
"paranoid": "Sécurité maximale pour les applications à haut risque.\n✓ Idéal pour : Banque, santé, apps critiques de conformité.\n⚠ AFFECTERA les apps mobiles, clients API et flux OAuth.\nUtilisez uniquement si vous comprenez et pouvez personnaliser chaque en-tête."
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "Gestion DNS",
|
||||
"description": "Gérer les fournisseurs DNS et les plugins pour l'automatisation des certificats"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"remoteServers": "远程服务器",
|
||||
"domains": "域名",
|
||||
"certificates": "证书",
|
||||
"dns": "DNS",
|
||||
"dnsProviders": "DNS 提供商",
|
||||
"plugins": "插件",
|
||||
"security": "安全",
|
||||
"accessLists": "访问列表",
|
||||
"crowdsec": "CrowdSec",
|
||||
@@ -976,5 +979,9 @@
|
||||
"strict": "Web 应用程序的强安全性。\n✓ 适用于:纯 Web 仪表板、管理面板。\n⚠ 可能会影响移动应用和 API 客户端。\n不推荐用于 Radarr、Plex 或带有配套应用的服务。",
|
||||
"paranoid": "高风险应用程序的最大安全性。\n✓ 适用于:银行、医疗、合规关键应用。\n⚠ 将会影响移动应用、API 客户端和 OAuth 流程。\n仅在您了解并能自定义每个头时使用。"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS 管理",
|
||||
"description": "管理 DNS 提供商和插件以实现证书自动化"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PageShell } from '../components/layout/PageShell'
|
||||
import { cn } from '../utils/cn'
|
||||
import { Cloud, Puzzle } from 'lucide-react'
|
||||
|
||||
export default function DNS() {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
|
||||
const isActive = (path: string) => location.pathname === path
|
||||
|
||||
const navItems = [
|
||||
{ path: '/dns/providers', label: t('navigation.dnsProviders'), icon: Cloud },
|
||||
{ path: '/dns/plugins', label: t('navigation.plugins'), icon: Puzzle },
|
||||
]
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title={t('dns.title')}
|
||||
description={t('dns.description')}
|
||||
actions={
|
||||
<div className="flex items-center gap-2 text-content-muted">
|
||||
<Cloud className="h-5 w-5" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* Tab Navigation */}
|
||||
<nav className="flex items-center gap-1 p-1 bg-surface-subtle rounded-lg w-fit">
|
||||
{navItems.map(({ path, label, icon: Icon }) => (
|
||||
<Link
|
||||
key={path}
|
||||
to={path}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-all duration-fast',
|
||||
isActive(path)
|
||||
? 'bg-surface-elevated text-content-primary shadow-sm'
|
||||
: 'text-content-secondary hover:text-content-primary hover:bg-surface-muted'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="bg-surface-elevated border border-border rounded-lg p-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</PageShell>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plus, Cloud } from 'lucide-react'
|
||||
import { PageShell } from '../components/layout/PageShell'
|
||||
import { Button, Alert, EmptyState, Skeleton } from '../components/ui'
|
||||
import DNSProviderCard from '../components/DNSProviderCard'
|
||||
import DNSProviderForm from '../components/DNSProviderForm'
|
||||
@@ -76,11 +75,12 @@ export default function DNSProviders() {
|
||||
)
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title={t('dnsProviders.title')}
|
||||
description={t('dnsProviders.description')}
|
||||
actions={headerActions}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Header with Add Button */}
|
||||
<div className="flex justify-end">
|
||||
{headerActions}
|
||||
</div>
|
||||
|
||||
{/* Info Alert */}
|
||||
<Alert variant="info" icon={Cloud}>
|
||||
<strong>{t('dnsProviders.note')}:</strong> {t('dnsProviders.noteText')}
|
||||
@@ -131,6 +131,6 @@ export default function DNSProviders() {
|
||||
provider={editingProvider}
|
||||
onSuccess={handleFormSuccess}
|
||||
/>
|
||||
</PageShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RefreshCw, Package, AlertCircle, CheckCircle, XCircle, Info } from 'lucide-react'
|
||||
import { PageShell } from '../components/layout/PageShell'
|
||||
import {
|
||||
Button,
|
||||
Badge,
|
||||
@@ -126,14 +125,12 @@ export default function Plugins() {
|
||||
)
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title={t('plugins.title', 'DNS Provider Plugins')}
|
||||
description={t(
|
||||
'plugins.description',
|
||||
'Manage built-in and external DNS provider plugins for certificate automation'
|
||||
)}
|
||||
actions={headerActions}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Header with Reload Button */}
|
||||
<div className="flex justify-end">
|
||||
{headerActions}
|
||||
</div>
|
||||
|
||||
{/* Info Alert */}
|
||||
<Alert variant="info" icon={Package}>
|
||||
<strong>{t('plugins.note', 'Note')}:</strong>{' '}
|
||||
@@ -387,6 +384,6 @@ export default function Plugins() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</PageShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { screen, within } from '@testing-library/react'
|
||||
import DNS from '../DNS'
|
||||
import { renderWithQueryClient } from '../../test-utils/renderWithQueryClient'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dns.title': 'DNS Management',
|
||||
'dns.description': 'Manage DNS providers and plugins for certificate automation',
|
||||
'navigation.dnsProviders': 'DNS Providers',
|
||||
'navigation.plugins': 'Plugins',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('DNS page', () => {
|
||||
it('renders DNS management page with navigation tabs', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
expect(await screen.findByText('DNS Management')).toBeInTheDocument()
|
||||
expect(screen.getByText('DNS Providers')).toBeInTheDocument()
|
||||
expect(screen.getByText('Plugins')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the navigation component', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
const nav = await screen.findByRole('navigation')
|
||||
expect(nav).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('highlights active tab based on route', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
const nav = await screen.findByRole('navigation')
|
||||
const providersLink = within(nav).getByText('DNS Providers').closest('a')
|
||||
|
||||
// Active tab should have the elevated style class
|
||||
expect(providersLink).toHaveClass('bg-surface-elevated')
|
||||
})
|
||||
|
||||
it('displays plugins tab as inactive when on providers route', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
const nav = await screen.findByRole('navigation')
|
||||
const pluginsLink = within(nav).getByText('Plugins').closest('a')
|
||||
|
||||
// Inactive tab should not have the elevated style class
|
||||
expect(pluginsLink).not.toHaveClass('bg-surface-elevated')
|
||||
expect(pluginsLink).toHaveClass('text-content-secondary')
|
||||
})
|
||||
|
||||
it('renders navigation links with correct paths', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
const nav = await screen.findByRole('navigation')
|
||||
const providersLink = within(nav).getByText('DNS Providers').closest('a')
|
||||
const pluginsLink = within(nav).getByText('Plugins').closest('a')
|
||||
|
||||
expect(providersLink).toHaveAttribute('href', '/dns/providers')
|
||||
expect(pluginsLink).toHaveAttribute('href', '/dns/plugins')
|
||||
})
|
||||
|
||||
it('renders content area for child routes', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
// The content area should be rendered with the border-border class
|
||||
const contentArea = document.querySelector('.bg-surface-elevated.border.border-border')
|
||||
expect(contentArea).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders cloud icon in header actions', async () => {
|
||||
renderWithQueryClient(<DNS />, { routeEntries: ['/dns/providers'] })
|
||||
|
||||
// Look for the Cloud icon in the header actions area
|
||||
const header = await screen.findByText('DNS Management')
|
||||
expect(header).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -121,9 +121,10 @@ describe('Plugins page', () => {
|
||||
it('renders plugin management page', async () => {
|
||||
renderWithQueryClient(<Plugins />)
|
||||
|
||||
expect(await screen.findByText('DNS Provider Plugins')).toBeInTheDocument()
|
||||
// Check that page renders without errors
|
||||
expect(screen.getByRole('button', { name: /reload plugins/i })).toBeInTheDocument()
|
||||
// The page now renders inside DNS parent which provides the PageShell
|
||||
// Check that page content renders without errors
|
||||
expect(await screen.findByRole('button', { name: /reload plugins/i })).toBeInTheDocument()
|
||||
expect(screen.getByText('Note:')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays built-in plugins section', async () => {
|
||||
|
||||
Reference in New Issue
Block a user