feat: Add Wild Central web app (Central-only UI)
Extract Central pages from the wild-cloud web app into a standalone React app for Wild Central. Includes: - Central overview, DNS, DHCP, Firewall, VPN, Ingress, CrowdSec pages - Simplified sidebar with Central-only navigation - Branding updated to "Wild Central" - All Cloud-specific pages, components, hooks, and API services removed - TypeScript type-check and production build pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
web/src/router/CentralLayout.tsx
Normal file
61
web/src/router/CentralLayout.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from '../components/ui/sidebar';
|
||||
import { HelpProvider, useHelp } from '../contexts/HelpContext';
|
||||
import { HelpPanel } from '../components/HelpPanel';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
|
||||
function CentralLayoutContent() {
|
||||
const { helpContent, setIsHelpOpen } = useHelp();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="min-w-0">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<h1 className="text-lg font-semibold">Wild Central</h1>
|
||||
</div>
|
||||
{helpContent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
className="ml-auto"
|
||||
>
|
||||
<HelpCircle className="h-5 w-5" />
|
||||
<span className="sr-only">Help</span>
|
||||
</Button>
|
||||
)}
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
<HelpPanel />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CentralLayout() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(
|
||||
() => localStorage.getItem('sidebar_state') !== 'false'
|
||||
);
|
||||
|
||||
return (
|
||||
<HelpProvider>
|
||||
<SidebarProvider
|
||||
open={sidebarOpen}
|
||||
onOpenChange={(open) => {
|
||||
setSidebarOpen(open);
|
||||
localStorage.setItem('sidebar_state', String(open));
|
||||
}}
|
||||
>
|
||||
<CentralLayoutContent />
|
||||
</SidebarProvider>
|
||||
</HelpProvider>
|
||||
);
|
||||
}
|
||||
12
web/src/router/index.tsx
Normal file
12
web/src/router/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createBrowserRouter } from 'react-router';
|
||||
import { routes } from './routes';
|
||||
|
||||
export const router = createBrowserRouter(routes, {
|
||||
future: {
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
},
|
||||
});
|
||||
|
||||
export { routes };
|
||||
export * from './pages/NotFoundPage';
|
||||
10
web/src/router/pages/CentralPage.tsx
Normal file
10
web/src/router/pages/CentralPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { CentralComponent } from '../../components/CentralComponent';
|
||||
|
||||
export function CentralPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CentralComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/CloudflarePage.tsx
Normal file
10
web/src/router/pages/CloudflarePage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { CloudflareComponent } from '../../components/CloudflareComponent';
|
||||
|
||||
export function CloudflarePage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CloudflareComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/CrowdSecPage.tsx
Normal file
10
web/src/router/pages/CrowdSecPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { CrowdSecComponent } from '../../components/CrowdSecComponent';
|
||||
|
||||
export function CrowdSecPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CrowdSecComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/DdnsPage.tsx
Normal file
10
web/src/router/pages/DdnsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DdnsComponent } from '../../components/DdnsComponent';
|
||||
|
||||
export function DdnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DdnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/DhcpPage.tsx
Normal file
10
web/src/router/pages/DhcpPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DhcpComponent } from '../../components/DhcpComponent';
|
||||
|
||||
export function DhcpPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DhcpComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/DnsPage.tsx
Normal file
10
web/src/router/pages/DnsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DnsComponent } from '../../components/DnsComponent';
|
||||
|
||||
export function DnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/FirewallPage.tsx
Normal file
10
web/src/router/pages/FirewallPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { FirewallComponent } from '../../components/FirewallComponent';
|
||||
|
||||
export function FirewallPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<FirewallComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/IngressProxyPage.tsx
Normal file
10
web/src/router/pages/IngressProxyPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { IngressProxyComponent } from '../../components/IngressProxyComponent';
|
||||
|
||||
export function IngressProxyPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<IngressProxyComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/LanDnsPage.tsx
Normal file
10
web/src/router/pages/LanDnsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { LanDnsComponent } from '../../components/LanDnsComponent';
|
||||
|
||||
export function LanDnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<LanDnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
30
web/src/router/pages/NotFoundPage.tsx
Normal file
30
web/src/router/pages/NotFoundPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Link } from 'react-router';
|
||||
import { AlertCircle, Home } from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[600px]">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<AlertCircle className="h-16 w-16 text-destructive" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Page Not Found</CardTitle>
|
||||
<CardDescription>
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center">
|
||||
<Link to="/">
|
||||
<Button>
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
Go to Home
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/VpnPage.tsx
Normal file
10
web/src/router/pages/VpnPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { VpnComponent } from '../../components/VpnComponent';
|
||||
|
||||
export function VpnPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<VpnComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
41
web/src/router/routes.tsx
Normal file
41
web/src/router/routes.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Navigate } from 'react-router';
|
||||
import type { RouteObject } from 'react-router';
|
||||
import { CentralLayout } from './CentralLayout';
|
||||
import { NotFoundPage } from './pages/NotFoundPage';
|
||||
import { CentralPage } from './pages/CentralPage';
|
||||
import { DhcpPage } from './pages/DhcpPage';
|
||||
import { FirewallPage } from './pages/FirewallPage';
|
||||
import { IngressProxyPage } from './pages/IngressProxyPage';
|
||||
import { DnsPage } from './pages/DnsPage';
|
||||
import { CrowdSecPage } from './pages/CrowdSecPage';
|
||||
import { VpnPage } from './pages/VpnPage';
|
||||
import { CloudflarePage } from './pages/CloudflarePage';
|
||||
import { DdnsPage } from './pages/DdnsPage';
|
||||
import { LanDnsPage } from './pages/LanDnsPage';
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
element: <Navigate to="/central" replace />,
|
||||
},
|
||||
{
|
||||
path: '/central',
|
||||
element: <CentralLayout />,
|
||||
children: [
|
||||
{ index: true, element: <CentralPage /> },
|
||||
{ path: 'cloudflare', element: <CloudflarePage /> },
|
||||
{ path: 'firewall', element: <FirewallPage /> },
|
||||
{ path: 'dhcp', element: <DhcpPage /> },
|
||||
{ path: 'crowdsec', element: <CrowdSecPage /> },
|
||||
{ path: 'ingress', element: <IngressProxyPage /> },
|
||||
{ path: 'dns', element: <DnsPage /> },
|
||||
{ path: 'ddns', element: <DdnsPage /> },
|
||||
{ path: 'lan-dns', element: <LanDnsPage /> },
|
||||
{ path: 'vpn', element: <VpnPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFoundPage />,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user