"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { MoreHorizontal, Network, ArrowRight } from "lucide-react"; import type { L4ProxyHost } from "@/src/lib/models/l4-proxy-hosts"; import { toggleL4ProxyHostAction } 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 { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { CreateL4HostDialog, EditL4HostDialog, DeleteL4HostDialog } from "@/components/l4-proxy-hosts/L4HostDialogs"; import { L4PortsApplyBanner } from "@/components/l4-proxy-hosts/L4PortsApplyBanner"; type Props = { hosts: L4ProxyHost[]; pagination: { total: number; page: number; perPage: number }; initialSearch: string; initialSort?: { sortBy: string; sortDir: "asc" | "desc" }; }; function formatMatcher(host: L4ProxyHost): string { switch (host.matcherType) { case "tls_sni": return `SNI: ${host.matcherValue.join(", ")}`; case "http_host": return `Host: ${host.matcherValue.join(", ")}`; case "proxy_protocol": return "Proxy Protocol"; default: return "None"; } } function ProtocolBadge({ protocol }: { protocol: string }) { if (protocol === "tcp") { return {protocol.toUpperCase()}; } return {protocol.toUpperCase()}; } export default function L4ProxyHostsClient({ hosts, pagination, initialSearch, initialSort }: Props) { const [createOpen, setCreateOpen] = useState(false); const [duplicateHost, setDuplicateHost] = useState(null); const [editHost, setEditHost] = useState(null); const [deleteHost, setDeleteHost] = useState(null); const [searchTerm, setSearchTerm] = useState(initialSearch); const [bannerRefresh, setBannerRefresh] = useState(0); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const debounceRef = useRef | null>(null); const signalBannerRefresh = () => setBannerRefresh(n => n + 1); 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 toggleL4ProxyHostAction(id, enabled); signalBannerRefresh(); }; const columns = [ { id: "name", label: "Name / Matcher", sortKey: "name", render: (host: L4ProxyHost) => ( {host.name} {formatMatcher(host)} ), }, { id: "protocol", label: "Protocol", sortKey: "protocol", width: 90, render: (host: L4ProxyHost) => , }, { id: "listen", label: "Listen", sortKey: "listenAddress", render: (host: L4ProxyHost) => ( {host.listenAddress} ), }, { id: "upstreams", label: "Upstreams", render: (host: L4ProxyHost) => ( {host.upstreams[0]} {host.upstreams.length > 1 && ( +{host.upstreams.length - 1} )} ), }, { id: "status", label: "Status", sortKey: "enabled", width: 110, render: (host: L4ProxyHost) => ( ), }, { id: "actions", label: "", align: "right" as const, width: 80, render: (host: L4ProxyHost) => ( handleToggleEnabled(host.id, checked)} /> Open menu setEditHost(host)}>Edit { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate setDeleteHost(host)} > Delete ), }, ]; const mobileCard = (host: L4ProxyHost) => ( {host.name} {host.listenAddress} → {host.upstreams[0]}{host.upstreams.length > 1 ? ` +${host.upstreams.length - 1}` : ""} handleToggleEnabled(host.id, checked)} /> Open menu setEditHost(host)}>Edit { setDuplicateHost(host); setCreateOpen(true); }}>Duplicate setDeleteHost(host)}>Delete ); return ( setCreateOpen(true) }} /> handleSearchChange(e.target.value)} placeholder="Search L4 hosts..." /> host.enabled ? "" : "opacity-75"} /> { setCreateOpen(false); setTimeout(() => setDuplicateHost(null), 200); signalBannerRefresh(); }} initialData={duplicateHost} /> {editHost && ( { setEditHost(null); signalBannerRefresh(); }} /> )} {deleteHost && ( { setDeleteHost(null); signalBannerRefresh(); }} /> )} ); }
{host.name}
{formatMatcher(host)}
{host.listenAddress} → {host.upstreams[0]}{host.upstreams.length > 1 ? ` +${host.upstreams.length - 1}` : ""}