Added domain form func.
This commit is contained in:
@@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Trash2, CheckCircle, AlertCircle, Loader2, Plus, Undo2, Save } from 'lucide-react';
|
import { Trash2, CheckCircle, AlertCircle, Loader2, Plus, Undo2, Save, RotateCw } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { DomainTopology } from './DomainTopology';
|
import { DomainTopology } from './DomainTopology';
|
||||||
@@ -109,16 +109,33 @@ function describeChanges(server: DomainDraft, draft: DomainDraft): string[] {
|
|||||||
|
|
||||||
/** Compact controls for mobile */
|
/** Compact controls for mobile */
|
||||||
function MobileControls({
|
function MobileControls({
|
||||||
draft, onUpdateDraft, cert, onProvisionCert, isProvisioningCert,
|
draft, onUpdateDraft, domain, cert, ddnsRecord,
|
||||||
|
onProvisionCert, onRenewCert, isProvisioningCert, isRenewingCert,
|
||||||
}: {
|
}: {
|
||||||
draft: DomainDraft;
|
draft: DomainDraft;
|
||||||
onUpdateDraft: (patch: Partial<DomainDraft>) => void;
|
onUpdateDraft: (patch: Partial<DomainDraft>) => void;
|
||||||
|
domain: RegisteredDomain;
|
||||||
cert: CertEntry | undefined;
|
cert: CertEntry | undefined;
|
||||||
|
ddnsRecord: DdnsRecordStatus | undefined;
|
||||||
onProvisionCert: (d: string) => void;
|
onProvisionCert: (d: string) => void;
|
||||||
|
onRenewCert: () => void;
|
||||||
isProvisioningCert: boolean;
|
isProvisioningCert: boolean;
|
||||||
|
isRenewingCert: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isL7 = draft.mode === 'l7';
|
const isL7 = draft.mode === 'l7';
|
||||||
|
const isDnsOnly = draft.mode === 'dns-only';
|
||||||
const hasCert = !!cert?.cert.exists;
|
const hasCert = !!cert?.cert.exists;
|
||||||
|
const daysLeft = cert?.cert.daysLeft ?? 0;
|
||||||
|
const hasRoutes = !!domain.routes?.length;
|
||||||
|
|
||||||
|
const [editingBackend, setEditingBackend] = useState(false);
|
||||||
|
const backendInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleBackendSave = () => {
|
||||||
|
const val = backendInputRef.current?.value.trim();
|
||||||
|
if (val) onUpdateDraft({ backendAddress: val });
|
||||||
|
setEditingBackend(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-3 py-2">
|
||||||
@@ -173,13 +190,87 @@ function MobileControls({
|
|||||||
Provision
|
Provision
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{hasCert && daysLeft < 30 && (
|
||||||
|
<Button variant="outline" size="sm" className="h-7 text-xs gap-1"
|
||||||
|
disabled={isRenewingCert}
|
||||||
|
onClick={() => onRenewCert()}
|
||||||
|
>
|
||||||
|
{isRenewingCert ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCw className="h-3 w-3" />}
|
||||||
|
Renew
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<span className="text-muted-foreground">Backend: </span>
|
<span className="text-muted-foreground">Backend: </span>
|
||||||
<span className="font-mono">{draft.backendAddress}</span>
|
{editingBackend ? (
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
ref={backendInputRef}
|
||||||
|
defaultValue={draft.backendAddress}
|
||||||
|
className="font-mono bg-muted rounded px-1.5 py-0.5 text-xs w-40 border border-border focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleBackendSave();
|
||||||
|
if (e.key === 'Escape') setEditingBackend(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button variant="ghost" size="sm" className="h-5 text-[10px] px-1" onClick={handleBackendSave}>Save</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="h-5 text-[10px] px-1" onClick={() => setEditingBackend(false)}>Cancel</Button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn('font-mono', !hasRoutes && 'underline decoration-dotted cursor-pointer hover:text-foreground')}
|
||||||
|
onClick={!hasRoutes ? () => setEditingBackend(true) : undefined}
|
||||||
|
disabled={hasRoutes}
|
||||||
|
>
|
||||||
|
{hasRoutes ? `${domain.routes!.length} backends` : draft.backendAddress}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Route table */}
|
||||||
|
{isL7 && domain.routes && domain.routes.length > 0 && (
|
||||||
|
<div className="bg-muted/30 rounded-md p-2 space-y-1">
|
||||||
|
<div className="text-[10px] text-muted-foreground font-medium mb-1">Route table</div>
|
||||||
|
{domain.routes.map((route, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-[10px] flex-wrap">
|
||||||
|
<span className="font-mono text-muted-foreground truncate">
|
||||||
|
{route.paths?.length ? route.paths.join(', ') : '/*'}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">{'->'}</span>
|
||||||
|
<span className="font-mono">{route.backend.address}</span>
|
||||||
|
{route.ipAllow && route.ipAllow.length > 0 && (
|
||||||
|
<span className="text-muted-foreground flex items-center gap-0.5">
|
||||||
|
{route.ipAllow.length} IP rules
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{isDnsOnly && (
|
||||||
|
<div className="flex items-center gap-2 text-[10px] text-amber-600 dark:text-amber-400 bg-amber-50/50 dark:bg-amber-950/20 rounded-md px-3 py-1.5">
|
||||||
|
<AlertCircle className="h-3 w-3 shrink-0" />
|
||||||
|
No firewall, proxy, or TLS protection — traffic goes directly to your server
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isL7 && !hasCert && (
|
||||||
|
<div className="flex items-center gap-2 text-[10px] text-red-600 dark:text-red-400 bg-red-50/50 dark:bg-red-950/20 rounded-md px-3 py-1.5">
|
||||||
|
<AlertCircle className="h-3 w-3 shrink-0" />
|
||||||
|
L7 routing is inactive until a TLS certificate is provisioned
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{draft.public && ddnsRecord && !ddnsRecord.ok && (
|
||||||
|
<div className="flex items-center gap-2 text-[10px] text-red-600 dark:text-red-400 bg-red-50/50 dark:bg-red-950/20 rounded-md px-3 py-1.5">
|
||||||
|
<AlertCircle className="h-3 w-3 shrink-0" />
|
||||||
|
DDNS sync failed{ddnsRecord.error ? `: ${ddnsRecord.error}` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -362,9 +453,13 @@ export function DomainCard({
|
|||||||
<MobileControls
|
<MobileControls
|
||||||
draft={draft}
|
draft={draft}
|
||||||
onUpdateDraft={updateDraft}
|
onUpdateDraft={updateDraft}
|
||||||
|
domain={domain}
|
||||||
cert={cert}
|
cert={cert}
|
||||||
|
ddnsRecord={ddnsRecord}
|
||||||
onProvisionCert={d => onProvisionCert(draft.subdomains ? `*.${domain.domain}` : d || domain.domain)}
|
onProvisionCert={d => onProvisionCert(draft.subdomains ? `*.${domain.domain}` : d || domain.domain)}
|
||||||
|
onRenewCert={onRenewCert}
|
||||||
isProvisioningCert={isProvisioningCert}
|
isProvisioningCert={isProvisioningCert}
|
||||||
|
isRenewingCert={isRenewingCert}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DomainTopology
|
<DomainTopology
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ function FlowNode({ data }: NodeProps) {
|
|||||||
<>
|
<>
|
||||||
{leftHandle}
|
{leftHandle}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Link to="/central/firewall" className="group nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
<Link to="/firewall" className="group nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
<TopologyNode
|
<TopologyNode
|
||||||
label="Firewall"
|
label="Firewall"
|
||||||
status={fwOk ? 'ok' : 'na'}
|
status={fwOk ? 'ok' : 'na'}
|
||||||
@@ -177,7 +177,7 @@ function FlowNode({ data }: NodeProps) {
|
|||||||
className="group-hover:border-primary/40"
|
className="group-hover:border-primary/40"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/central/crowdsec" className="group nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
<Link to="/crowdsec" className="group nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
<TopologyNode
|
<TopologyNode
|
||||||
label="CrowdSec"
|
label="CrowdSec"
|
||||||
sublabel={isL4 ? 'IP only' : 'HTTP + IP'}
|
sublabel={isL4 ? 'IP only' : 'HTTP + IP'}
|
||||||
@@ -317,7 +317,7 @@ function FlowNode({ data }: NodeProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{leftHandle}
|
{leftHandle}
|
||||||
<Link to="/central/vpn" className="nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
<Link to="/vpn" className="nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
<TopologyNode
|
<TopologyNode
|
||||||
label="VPN"
|
label="VPN"
|
||||||
sublabel={running ? `${peerCount} peer${peerCount !== 1 ? 's' : ''}` : 'Not running'}
|
sublabel={running ? `${peerCount} peer${peerCount !== 1 ? 's' : ''}` : 'Not running'}
|
||||||
@@ -336,7 +336,7 @@ function FlowNode({ data }: NodeProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{leftHandle}
|
{leftHandle}
|
||||||
<Link to="/central" className="nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
<Link to="/" className="nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
<TopologyNode
|
<TopologyNode
|
||||||
label="Router"
|
label="Router"
|
||||||
sublabel={ports}
|
sublabel={ports}
|
||||||
|
|||||||
Reference in New Issue
Block a user