"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Globe, MoreHorizontal, ArrowRight, Shield, Bug, MapPin, Scale, KeyRound, UserCheck, CornerRightDown, Replace } from "lucide-react"; import type { AccessList } from "@/lib/models/access-lists"; import type { Certificate } from "@/lib/models/certificates"; import type { ProxyHost } from "@/lib/models/proxy-hosts"; import type { CaCertificate } from "@/lib/models/ca-certificates"; import type { AuthentikSettings } from "@/lib/settings"; import type { MtlsRole } from "@/lib/models/mtls-roles"; import type { IssuedClientCertificate } from "@/lib/models/issued-client-certificates"; import { toggleProxyHostAction } from "./actions"; import { PageHeader } from "@/components/ui/PageHeader"; import { SearchField } from "@/components/ui/SearchField"; import { DataTable } from "@/components/ui/DataTable"; import { StatusChip } from "@/components/ui/StatusChip"; import { CreateHostDialog, EditHostDialog, DeleteHostDialog } from "@/components/proxy-hosts/HostDialogs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; type ForwardAuthUser = { id: number; email: string; name: string | null; role: string }; type ForwardAuthGroup = { id: number; name: string; description: string | null; member_count: number }; type ForwardAuthAccessMap = Record; type Props = { hosts: ProxyHost[]; certificates: Certificate[]; accessLists: AccessList[]; caCertificates: CaCertificate[]; authentikDefaults: AuthentikSettings | null; pagination: { total: number; page: number; perPage: number }; initialSearch: string; initialSort?: { sortBy: string; sortDir: "asc" | "desc" }; mtlsRoles?: MtlsRole[]; issuedClientCerts?: IssuedClientCertificate[]; forwardAuthUsers?: ForwardAuthUser[]; forwardAuthGroups?: ForwardAuthGroup[]; forwardAuthAccessMap?: ForwardAuthAccessMap; }; export default function ProxyHostsClient({ hosts, certificates, accessLists, caCertificates, authentikDefaults, pagination, initialSearch, initialSort, mtlsRoles, issuedClientCerts, forwardAuthUsers, forwardAuthGroups, forwardAuthAccessMap }: Props) { const [createOpen, setCreateOpen] = useState(false); const [duplicateHost, setDuplicateHost] = useState(null); const [editHost, setEditHost] = useState(null); const [deleteHost, setDeleteHost] = useState(null); // Counter forces CreateHostDialog to remount on each open, resetting useFormState const [dialogKey, setDialogKey] = useState(0); const [searchTerm, setSearchTerm] = useState(initialSearch); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const debounceRef = useRef | null>(null); useEffect(() => { setSearchTerm(initialSearch); }, [initialSearch]); function handleSearchChange(value: string) { setSearchTerm(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { const params = new URLSearchParams(searchParams.toString()); if (value.trim()) { params.set("search", value.trim()); } else { params.delete("search"); } params.set("page", "1"); router.push(`${pathname}?${params.toString()}`); }, 400); } const handleToggleEnabled = async (id: number, enabled: boolean) => { await toggleProxyHostAction(id, enabled); }; const columns = [ { id: "name", label: "Name / Domain", sortKey: "name", render: (host: ProxyHost) => (

{host.name}

{host.domains[0]} {host.domains.length > 1 && ( +{host.domains.length - 1} )}

), }, { id: "target", label: "Upstream", sortKey: "upstreams", render: (host: ProxyHost) => (
{host.upstreams[0]} {host.upstreams.length > 1 && ( +{host.upstreams.length - 1} )}
), }, { id: "features", label: "Features", render: (host: ProxyHost) => { const badges = [ host.certificateId && ( TLS ), host.accessListId && ( Auth ), host.authentik?.enabled && ( Authentik ), host.waf?.enabled && ( WAF ), host.geoblock?.enabled && ( Geo ), host.loadBalancer?.enabled && ( LB ), host.mtls?.enabled && ( mTLS ), host.redirects?.length > 0 && ( Redirects ), host.rewrite && ( Rewrite ), ].filter(Boolean); return (
{badges.length > 0 ? badges : }
); }, }, { id: "status", label: "Status", sortKey: "enabled", width: 110, render: (host: ProxyHost) => ( ), }, { id: "actions", label: "", align: "right" as const, width: 80, render: (host: ProxyHost) => (
handleToggleEnabled(host.id, checked)} /> setEditHost(host)}>Edit { setDuplicateHost(host); { setDialogKey(k => k + 1); setCreateOpen(true); }; }}>Duplicate setDeleteHost(host)} > Delete
), }, ]; const mobileCard = (host: ProxyHost) => (

{host.name}

{host.domains[0]}{host.domains.length > 1 ? ` +${host.domains.length - 1}` : ""} {host.upstreams[0]}

{host.certificateId && TLS}
handleToggleEnabled(host.id, checked)} /> setEditHost(host)}>Edit { setDuplicateHost(host); { setDialogKey(k => k + 1); setCreateOpen(true); }; }}>Duplicate setDeleteHost(host)}>Delete
); return (
{ setDialogKey(k => k + 1); setCreateOpen(true); } }} />
handleSearchChange(e.target.value)} placeholder="Search hosts..." />
host.enabled ? "" : "opacity-75"} /> { setCreateOpen(false); setTimeout(() => setDuplicateHost(null), 200); }} initialData={duplicateHost} certificates={certificates} accessLists={accessLists} authentikDefaults={authentikDefaults} caCertificates={caCertificates} mtlsRoles={mtlsRoles ?? []} issuedClientCerts={issuedClientCerts ?? []} forwardAuthUsers={forwardAuthUsers ?? []} forwardAuthGroups={forwardAuthGroups ?? []} /> {editHost && ( setEditHost(null)} certificates={certificates} accessLists={accessLists} caCertificates={caCertificates} mtlsRoles={mtlsRoles ?? []} issuedClientCerts={issuedClientCerts ?? []} forwardAuthUsers={forwardAuthUsers ?? []} forwardAuthGroups={forwardAuthGroups ?? []} forwardAuthAccess={forwardAuthAccessMap?.[editHost.id] ?? null} /> )} {deleteHost && ( setDeleteHost(null)} /> )}
); }