feat: UI refactor — Dashboard + Services, 9 items → 6
Major restructure of Wild Central's web UI: New pages: - Dashboard: infrastructure health (Cloudflare token, DDNS sync, daemon status, gateway router guidance, cert warnings) - Services: every registered service with expandable rows showing DNS, proxy, TLS, DDNS status. Custom TCP routes section. Advanced HAProxy management collapsed at bottom. Sidebar restructured into two groups: - Services: Dashboard, Services - Central: Firewall, VPN, CrowdSec, DHCP Deleted 7 pages + 7 components (2501 lines removed): - CentralPage, CloudflarePage, DdnsPage, DnsPage, LanDnsPage, CertificatesPage, IngressProxyPage - CentralComponent, CloudflareComponent, DdnsComponent, DnsComponent, LanDnsComponent, IngressProxyComponent, DnsmasqSection All hooks and API services kept unchanged — reused in new components. Type-check and production build pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { CentralComponent } from '../../components/CentralComponent';
|
||||
|
||||
export function CentralPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CentralComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Input, Label } from '../../components/ui';
|
||||
import { Alert, AlertDescription } from '../../components/ui/alert';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { Shield, Loader2, CheckCircle, AlertCircle, RefreshCw, Plus, ShieldCheck, ShieldAlert, X } from 'lucide-react';
|
||||
import { useCert } from '../../hooks/useCert';
|
||||
import { usePageHelp } from '../../hooks/usePageHelp';
|
||||
|
||||
export function CertificatesPage() {
|
||||
const {
|
||||
status: certStatus,
|
||||
isLoading,
|
||||
provision: provisionCert,
|
||||
isProvisioning,
|
||||
renew: renewCerts,
|
||||
isRenewing,
|
||||
} = useCert();
|
||||
const [provisioningDomain, setProvisioningDomain] = useState<string | null>(null);
|
||||
const [showWildcardForm, setShowWildcardForm] = useState(false);
|
||||
const [wildcardInput, setWildcardInput] = useState('');
|
||||
|
||||
usePageHelp({
|
||||
title: 'TLS Certificates',
|
||||
description: (
|
||||
<p className="leading-relaxed">
|
||||
Wild Central provisions TLS certificates for services that need HTTPS.
|
||||
Each service gets its own certificate by default. You can also provision
|
||||
a wildcard certificate to cover multiple services under the same domain.
|
||||
</p>
|
||||
),
|
||||
});
|
||||
|
||||
const certs = certStatus?.certs ?? [];
|
||||
const canProvision = certStatus?.canProvision ?? false;
|
||||
|
||||
const handleProvision = async (domain: string) => {
|
||||
setProvisioningDomain(domain);
|
||||
try {
|
||||
await provisionCert(domain);
|
||||
} finally {
|
||||
setProvisioningDomain(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProvisionWildcard = async () => {
|
||||
if (!wildcardInput) return;
|
||||
const domain = wildcardInput.startsWith('*.') ? wildcardInput : `*.${wildcardInput}`;
|
||||
setProvisioningDomain(domain);
|
||||
try {
|
||||
await provisionCert(domain);
|
||||
setShowWildcardForm(false);
|
||||
setWildcardInput('');
|
||||
} finally {
|
||||
setProvisioningDomain(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="p-8 text-center">
|
||||
<Loader2 className="h-12 w-12 text-primary mx-auto mb-4 animate-spin" />
|
||||
<p className="text-muted-foreground">Loading certificate status...</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const allExist = certs.length > 0 && certs.every((c) => c.cert?.exists || c.coveredBy);
|
||||
const someExist = certs.some((c) => c.cert?.exists || c.coveredBy);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">TLS Certificates</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage HTTPS certificates for registered services
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{allExist ? (
|
||||
<Badge variant="success" className="gap-1"><ShieldCheck className="h-3 w-3" />All Valid</Badge>
|
||||
) : someExist ? (
|
||||
<Badge variant="warning" className="gap-1"><ShieldAlert className="h-3 w-3" />Incomplete</Badge>
|
||||
) : certs.length > 0 ? (
|
||||
<Badge variant="destructive" className="gap-1"><ShieldAlert className="h-3 w-3" />Missing</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!canProvision && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Certificate provisioning requires a Cloudflare API token and operator email.
|
||||
{!certStatus?.hasToken && ' Configure the token on the Cloudflare page.'}
|
||||
{!certStatus?.hasEmail && ' Set the operator email in Overview.'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{certs.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Shield className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No Services Registered</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Register services with Wild Central to manage their certificates here.
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Service Certificates</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
{canProvision && (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowWildcardForm(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />Wildcard
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={() => renewCerts()} disabled={isRenewing || !someExist}>
|
||||
{isRenewing ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <RefreshCw className="h-4 w-4 mr-1" />}
|
||||
Renew All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{showWildcardForm && (
|
||||
<div className="p-3 border rounded-lg bg-muted/50 space-y-2">
|
||||
<Label>Provision Wildcard Certificate</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<span className="text-sm text-muted-foreground">*.</span>
|
||||
<Input
|
||||
value={wildcardInput}
|
||||
onChange={(e) => setWildcardInput(e.target.value)}
|
||||
placeholder="cloud.payne.io"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" onClick={handleProvisionWildcard} disabled={isProvisioning || !wildcardInput}>
|
||||
{provisioningDomain?.startsWith('*.') ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Plus className="h-4 w-4 mr-1" />}
|
||||
Provision
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setShowWildcardForm(false); setWildcardInput(''); }}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
A wildcard cert covers all subdomains. E.g., *.cloud.payne.io covers app1.cloud.payne.io, app2.cloud.payne.io, etc.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border rounded-lg divide-y">
|
||||
{certs.map((entry) => {
|
||||
const exists = entry.cert?.exists;
|
||||
const covered = entry.coveredBy;
|
||||
const isThisProvisioning = provisioningDomain === entry.domain;
|
||||
|
||||
return (
|
||||
<div key={entry.domain} className="px-4 py-3 flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-sm truncate">{entry.domain}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-muted-foreground">{entry.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 ml-4">
|
||||
{exists ? (
|
||||
<Badge variant="success" className="gap-1">
|
||||
<ShieldCheck className="h-3 w-3" />{entry.cert.daysLeft}d
|
||||
</Badge>
|
||||
) : covered ? (
|
||||
<Badge variant="outline" className="gap-1 text-green-600 border-green-300">
|
||||
<CheckCircle className="h-3 w-3" />{covered}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="destructive" className="gap-1">Missing</Badge>
|
||||
{canProvision && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleProvision(entry.domain)} disabled={isProvisioning}>
|
||||
{isThisProvisioning ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Plus className="h-3 w-3 mr-1" />}
|
||||
Provision
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { CloudflareComponent } from '../../components/CloudflareComponent';
|
||||
|
||||
export function CloudflarePage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CloudflareComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/DashboardPage.tsx
Normal file
10
web/src/router/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DashboardComponent } from '../../components/DashboardComponent';
|
||||
|
||||
export function DashboardPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DashboardComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DdnsComponent } from '../../components/DdnsComponent';
|
||||
|
||||
export function DdnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DdnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DnsComponent } from '../../components/DnsComponent';
|
||||
|
||||
export function DnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { IngressProxyComponent } from '../../components/IngressProxyComponent';
|
||||
|
||||
export function IngressProxyPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<IngressProxyComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { LanDnsComponent } from '../../components/LanDnsComponent';
|
||||
|
||||
export function LanDnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<LanDnsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/ServicesPage.tsx
Normal file
10
web/src/router/pages/ServicesPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { ServicesComponent } from '../../components/ServicesComponent';
|
||||
|
||||
export function ServicesPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ServicesComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user