"use client"; import { useEffect, useState, use } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { AxiosResponse } from "axios"; import { ListTargetsResponse } from "@server/routers/target/listTargets"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { CreateTargetResponse } from "@server/routers/target"; import { ColumnDef, getFilteredRowModel, getSortedRowModel, getPaginationRowModel, getCoreRowModel, useReactTable, flexRender, Row } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@app/components/ui/table"; import { toast } from "@app/hooks/useToast"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { ArrayElement } from "@server/types/ArrayElement"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient } from "@app/lib/api"; import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; import { SettingsContainer, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; import { tlsNameSchema } from "@server/lib/schemas"; import { CheckIcon, ChevronsUpDown, Settings, Heart, Check, CircleCheck, CircleX, ArrowRight, Plus, MoveRight, ArrowUp, Info, ArrowDown, AlertTriangle } from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import HealthCheckDialog from "@/components/HealthCheckDialog"; import { DockerManager, DockerState } from "@app/lib/docker"; import { Container } from "@server/routers/site"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { cn } from "@app/lib/cn"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { HeadersInput } from "@app/components/HeadersInput"; import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; import { Badge } from "@app/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; import { Alert, AlertDescription } from "@app/components/ui/alert"; const addTargetSchema = z .object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive(), siteId: z.int() .positive({ error: "You must select a site for a target." }), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) .optional() .nullable(), rewritePath: z.string().optional().nullable(), rewritePathType: z .enum(["exact", "prefix", "regex", "stripPrefix"]) .optional() .nullable(), priority: z.int().min(1).max(1000).optional() }) .refine( (data) => { // If path is provided, pathMatchType must be provided if (data.path && !data.pathMatchType) { return false; } // If pathMatchType is provided, path must be provided if (data.pathMatchType && !data.path) { return false; } // Validate path based on pathMatchType if (data.path && data.pathMatchType) { switch (data.pathMatchType) { case "exact": case "prefix": // Path should start with / return data.path.startsWith("/"); case "regex": // Validate regex try { new RegExp(data.path); return true; } catch { return false; } } } return true; }, { error: "Invalid path configuration" } ) .refine( (data) => { // If rewritePath is provided, rewritePathType must be provided if (data.rewritePath && !data.rewritePathType) { return false; } // If rewritePathType is provided, rewritePath must be provided if (data.rewritePathType && !data.rewritePath) { return false; } return true; }, { error: "Invalid rewrite path configuration" } ); const targetsSettingsSchema = z.object({ stickySession: z.boolean() }); type LocalTarget = Omit< ArrayElement & { new?: boolean; updated?: boolean; siteType: string | null; }, "protocol" >; export default function ReverseProxyTargets(props: { params: Promise<{ resourceId: number; orgId: string }>; }) { const params = use(props.params); const t = useTranslations(); const { env } = useEnvContext(); const { resource, updateResource } = useResourceContext(); const api = createApiClient(useEnvContext()); const [targets, setTargets] = useState([]); const [targetsToRemove, setTargetsToRemove] = useState([]); const [sites, setSites] = useState([]); const [dockerStates, setDockerStates] = useState>( new Map() ); const initializeDockerForSite = async (siteId: number) => { if (dockerStates.has(siteId)) { return; // Already initialized } const dockerManager = new DockerManager(api, siteId); const dockerState = await dockerManager.initializeDocker(); setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; const refreshContainersForSite = async (siteId: number) => { const dockerManager = new DockerManager(api, siteId); const containers = await dockerManager.fetchContainers(); setDockerStates((prev) => { const newMap = new Map(prev); const existingState = newMap.get(siteId); if (existingState) { newMap.set(siteId, { ...existingState, containers }); } return newMap; }); }; const getDockerStateForSite = (siteId: number): DockerState => { return ( dockerStates.get(siteId) || { isEnabled: false, isAvailable: false, containers: [] } ); }; const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); const [targetsLoading, setTargetsLoading] = useState(false); const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("proxy-advanced-mode"); return saved === "true"; } return false; }); const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = useState(null); const router = useRouter(); const proxySettingsSchema = z.object({ setHostHeader: z .string() .optional() .refine( (data) => { if (data) { return tlsNameSchema.safeParse(data).success; } return true; }, { message: t("proxyErrorInvalidHeader") } ), headers: z .array(z.object({ name: z.string(), value: z.string() })) .nullable(), proxyProtocol: z.boolean().optional(), proxyProtocolVersion: z.int().min(1).max(2).optional() }); const tlsSettingsSchema = z.object({ ssl: z.boolean(), tlsServerName: z .string() .optional() .refine( (data) => { if (data) { return tlsNameSchema.safeParse(data).success; } return true; }, { message: t("proxyErrorTls") } ) }); type ProxySettingsValues = z.infer; type TlsSettingsValues = z.infer; type TargetsSettingsValues = z.infer; const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), defaultValues: { ssl: resource.ssl, tlsServerName: resource.tlsServerName || "" } }); const proxySettingsForm = useForm({ resolver: zodResolver(proxySettingsSchema), defaultValues: { setHostHeader: resource.setHostHeader || "", headers: resource.headers, proxyProtocol: resource.proxyProtocol || false, proxyProtocolVersion: resource.proxyProtocolVersion || 1 } }); const targetsSettingsForm = useForm({ resolver: zodResolver(targetsSettingsSchema), defaultValues: { stickySession: resource.stickySession } }); useEffect(() => { const fetchTargets = async () => { try { const res = await api.get>( `/resource/${resource.resourceId}/targets` ); if (res.status === 200) { setTargets(res.data.data.targets); } } catch (err) { console.error(err); toast({ variant: "destructive", title: t("targetErrorFetch"), description: formatAxiosError( err, t("targetErrorFetchDescription") ) }); } finally { setPageLoading(false); } }; fetchTargets(); const fetchSites = async () => { const res = await api .get< AxiosResponse >(`/org/${params.orgId}/sites`) .catch((e) => { toast({ variant: "destructive", title: t("sitesErrorFetch"), description: formatAxiosError( e, t("sitesErrorFetchDescription") ) }); }); if (res?.status === 200) { setSites(res.data.data.sites); // Initialize Docker for newt sites const newtSites = res.data.data.sites.filter( (site) => site.type === "newt" ); for (const site of newtSites) { initializeDockerForSite(site.siteId); } // Sites loaded successfully } }; fetchSites(); // const fetchSite = async () => { // try { // const res = await api.get>( // `/site/${resource.siteId}` // ); // // if (res.status === 200) { // setSite(res.data.data); // } // } catch (err) { // console.error(err); // toast({ // variant: "destructive", // title: t("siteErrorFetch"), // description: formatAxiosError( // err, // t("siteErrorFetchDescription") // ) // }); // } // }; // fetchSite(); }, []); // Save advanced mode preference to localStorage useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem( "proxy-advanced-mode", isAdvancedMode.toString() ); } }, [isAdvancedMode]); function addNewTarget() { const isHttp = resource.http; const newTarget: LocalTarget = { targetId: -Date.now(), // Use negative timestamp as temporary ID ip: "", method: isHttp ? "http" : null, port: 0, siteId: sites.length > 0 ? sites[0].siteId : 0, path: isHttp ? null : null, pathMatchType: isHttp ? null : null, rewritePath: isHttp ? null : null, rewritePathType: isHttp ? null : null, priority: isHttp ? 100 : 100, enabled: true, resourceId: resource.resourceId, hcEnabled: false, hcPath: null, hcMethod: null, hcInterval: null, hcTimeout: null, hcHeaders: null, hcScheme: null, hcHostname: null, hcPort: null, hcFollowRedirects: null, hcHealth: "unknown", hcStatus: null, hcMode: null, hcUnhealthyInterval: null, hcTlsServerName: null, siteType: sites.length > 0 ? sites[0].type : null, new: true, updated: false }; setTargets((prev) => [...prev, newTarget]); } async function saveNewTarget(target: LocalTarget) { // Validate the target if (!isTargetValid(target.ip)) { toast({ variant: "destructive", title: t("targetErrorInvalidIp"), description: t("targetErrorInvalidIpDescription") }); return; } if (!target.port || target.port <= 0) { toast({ variant: "destructive", title: t("targetErrorInvalidPort"), description: t("targetErrorInvalidPortDescription") }); return; } if (!target.siteId) { toast({ variant: "destructive", title: t("targetErrorNoSite"), description: t("targetErrorNoSiteDescription") }); return; } try { setTargetsLoading(true); const data: any = { resourceId: resource.resourceId, siteId: target.siteId, ip: target.ip, method: target.method, port: target.port, enabled: target.enabled, hcEnabled: target.hcEnabled, hcPath: target.hcPath || null, hcScheme: target.hcScheme || null, hcHostname: target.hcHostname || null, hcPort: target.hcPort || null, hcInterval: target.hcInterval || null, hcTimeout: target.hcTimeout || null, hcHeaders: target.hcHeaders || null, hcFollowRedirects: target.hcFollowRedirects || null, hcMethod: target.hcMethod || null, hcStatus: target.hcStatus || null, hcUnhealthyInterval: target.hcUnhealthyInterval || null, hcMode: target.hcMode || null }; // Only include path-related fields for HTTP resources if (resource.http) { data.path = target.path; data.pathMatchType = target.pathMatchType; data.rewritePath = target.rewritePath; data.rewritePathType = target.rewritePathType; data.priority = target.priority; } const response = await api.post< AxiosResponse >(`/target`, data); if (response.status === 200) { // Update the target with the new ID and remove the new flag setTargets((prev) => prev.map((t) => t.targetId === target.targetId ? { ...t, targetId: response.data.data.targetId, new: false, updated: false } : t ) ); toast({ title: t("targetCreated"), description: t("targetCreatedDescription") }); } } catch (err) { console.error(err); toast({ variant: "destructive", title: t("targetErrorCreate"), description: formatAxiosError( err, t("targetErrorCreateDescription") ) }); } finally { setTargetsLoading(false); } } async function addTarget(data: z.infer) { // if (site && site.type == "wireguard" && site.subnet) { // // make sure that the target IP is within the site subnet // const targetIp = data.ip; // const subnet = site.subnet; // try { // if (!isIPInSubnet(targetIp, subnet)) { // toast({ // variant: "destructive", // title: t("targetWireGuardErrorInvalidIp"), // description: t( // "targetWireGuardErrorInvalidIpDescription" // ) // }); // return; // } // } catch (error) { // console.error(error); // toast({ // variant: "destructive", // title: t("targetWireGuardErrorInvalidIp"), // description: t("targetWireGuardErrorInvalidIpDescription") // }); // return; // } // } const site = sites.find((site) => site.siteId === data.siteId); const isHttp = resource.http; const newTarget: LocalTarget = { ...data, path: isHttp ? (data.path || null) : null, pathMatchType: isHttp ? (data.pathMatchType || null) : null, rewritePath: isHttp ? (data.rewritePath || null) : null, rewritePathType: isHttp ? (data.rewritePathType || null) : null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, resourceId: resource.resourceId, priority: isHttp ? (data.priority || 100) : 100, hcEnabled: false, hcPath: null, hcMethod: null, hcInterval: null, hcTimeout: null, hcHeaders: null, hcScheme: null, hcHostname: null, hcPort: null, hcFollowRedirects: null, hcHealth: "unknown", hcStatus: null, hcMode: null, hcUnhealthyInterval: null, hcTlsServerName: null, }; setTargets([...targets, newTarget]); } const removeTarget = (targetId: number) => { setTargets([ ...targets.filter((target) => target.targetId !== targetId) ]); if (!targets.find((target) => target.targetId === targetId)?.new) { setTargetsToRemove([...targetsToRemove, targetId]); } }; async function updateTarget(targetId: number, data: Partial) { const site = sites.find((site) => site.siteId === data.siteId); setTargets( targets.map((target) => target.targetId === targetId ? { ...target, ...data, updated: true, siteType: site ? site.type : target.siteType } : target ) ); } function updateTargetHealthCheck(targetId: number, config: any) { setTargets( targets.map((target) => target.targetId === targetId ? { ...target, ...config, updated: true } : target ) ); } const openHealthCheckDialog = (target: LocalTarget) => { console.log(target); setSelectedTargetForHealthCheck(target); setHealthCheckDialogOpen(true); }; async function saveAllSettings() { // Validate that no targets have blank IPs or invalid ports const targetsWithInvalidFields = targets.filter( (target) => !target.ip || target.ip.trim() === "" || !target.port || target.port <= 0 || isNaN(target.port) ); if (targetsWithInvalidFields.length > 0) { toast({ variant: "destructive", title: t("targetErrorInvalidIp"), description: t("targetErrorInvalidIpDescription") }); return; } try { setTargetsLoading(true); setHttpsTlsLoading(true); setProxySettingsLoading(true); for (const targetId of targetsToRemove) { await api.delete(`/target/${targetId}`); } // Save targets for (const target of targets) { const data: any = { ip: target.ip, port: target.port, method: target.method, enabled: target.enabled, siteId: target.siteId, hcEnabled: target.hcEnabled, hcPath: target.hcPath || null, hcScheme: target.hcScheme || null, hcHostname: target.hcHostname || null, hcPort: target.hcPort || null, hcInterval: target.hcInterval || null, hcTimeout: target.hcTimeout || null, hcHeaders: target.hcHeaders || null, hcFollowRedirects: target.hcFollowRedirects || null, hcMethod: target.hcMethod || null, hcStatus: target.hcStatus || null, hcUnhealthyInterval: target.hcUnhealthyInterval || null, hcMode: target.hcMode || null, hcTlsServerName: target.hcTlsServerName, }; // Only include path-related fields for HTTP resources if (resource.http) { data.path = target.path; data.pathMatchType = target.pathMatchType; data.rewritePath = target.rewritePath; data.rewritePathType = target.rewritePathType; data.priority = target.priority; } if (target.new) { const res = await api.put< AxiosResponse >(`/resource/${resource.resourceId}/target`, data); target.targetId = res.data.data.targetId; target.new = false; } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); target.updated = false; } } if (resource.http) { // Gather all settings const stickySessionData = targetsSettingsForm.getValues(); const tlsData = tlsSettingsForm.getValues(); const proxyData = proxySettingsForm.getValues(); // Combine into one payload const payload = { stickySession: stickySessionData.stickySession, ssl: tlsData.ssl, tlsServerName: tlsData.tlsServerName || null, setHostHeader: proxyData.setHostHeader || null, headers: proxyData.headers || null }; // Single API call to update all settings await api.post(`/resource/${resource.resourceId}`, payload); // Update local resource context updateResource({ ...resource, stickySession: stickySessionData.stickySession, ssl: tlsData.ssl, tlsServerName: tlsData.tlsServerName || null, setHostHeader: proxyData.setHostHeader || null, headers: proxyData.headers || null }); } else { // For TCP/UDP resources, save proxy protocol settings const proxyData = proxySettingsForm.getValues(); const payload = { proxyProtocol: proxyData.proxyProtocol || false, proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 }; await api.post(`/resource/${resource.resourceId}`, payload); updateResource({ ...resource, proxyProtocol: proxyData.proxyProtocol || false, proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 }); } toast({ title: t("settingsUpdated"), description: t("settingsUpdatedDescription") }); setTargetsToRemove([]); router.refresh(); } catch (err) { console.error(err); toast({ variant: "destructive", title: t("settingsErrorUpdate"), description: formatAxiosError( err, t("settingsErrorUpdateDescription") ) }); } finally { setTargetsLoading(false); setHttpsTlsLoading(false); setProxySettingsLoading(false); } } const getColumns = (): ColumnDef[] => { const baseColumns: ColumnDef[] = []; const isHttp = resource.http; const priorityColumn: ColumnDef = { id: "priority", header: () => (
{t("priority")}

{t("priorityDescription")}

), cell: ({ row }) => { return (
e.currentTarget.focus()} defaultValue={row.original.priority || 100} className="w-full max-w-20" onBlur={(e) => { const value = parseInt(e.target.value, 10); if (value >= 1 && value <= 1000) { updateTarget(row.original.targetId, { ...row.original, priority: value }); } }} />
); }, size: 120, minSize: 100, maxSize: 150 }; const healthCheckColumn: ColumnDef = { accessorKey: "healthCheck", header: () => ({t("healthCheck")}), cell: ({ row }) => { const status = row.original.hcHealth || "unknown"; const isEnabled = row.original.hcEnabled; const getStatusColor = (status: string) => { switch (status) { case "healthy": return "green"; case "unhealthy": return "red"; case "unknown": default: return "secondary"; } }; const getStatusText = (status: string) => { switch (status) { case "healthy": return t("healthCheckHealthy"); case "unhealthy": return t("healthCheckUnhealthy"); case "unknown": default: return t("healthCheckUnknown"); } }; const getStatusIcon = (status: string) => { switch (status) { case "healthy": return ; case "unhealthy": return ; case "unknown": default: return null; } }; return (
{row.original.siteType === "newt" ? ( ) : ( - )}
); }, size: 200, minSize: 180, maxSize: 250 }; const matchPathColumn: ColumnDef = { accessorKey: "path", header: () => ({t("matchPath")}), cell: ({ row }) => { const hasPathMatch = !!( row.original.path || row.original.pathMatchType ); return (
{hasPathMatch ? ( updateTarget(row.original.targetId, config) } trigger={ } /> ) : ( updateTarget(row.original.targetId, config) } trigger={ } /> )}
); }, size: 200, minSize: 180, maxSize: 200 }; const addressColumn: ColumnDef = { accessorKey: "address", header: () => ({t("address")}), cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId ); const handleContainerSelectForTarget = ( hostname: string, port?: number ) => { updateTarget(row.original.targetId, { ...row.original, ip: hostname, ...(port && { port: port }) }); }; return (
{selectedSite && selectedSite.type === "newt" && (() => { const dockerState = getDockerStateForSite( selectedSite.siteId ); return ( refreshContainersForSite( selectedSite.siteId ) } /> ); })()} {t("siteNotFound")} {sites.map((site) => ( updateTarget( row.original .targetId, { siteId: site.siteId } ) } > {site.name} ))} {resource.http && ( )} {resource.http && (
{"://"}
)} { const input = e.target.value.trim(); const hasProtocol = /^(https?|h2c):\/\//.test(input); const hasPort = /:\d+(?:\/|$)/.test(input); if (hasProtocol || hasPort) { const parsed = parseHostTarget(input); if (parsed) { updateTarget( row.original.targetId, { ...row.original, method: hasProtocol ? parsed.protocol : row.original.method, ip: parsed.host, port: hasPort ? parsed.port : row.original.port } ); } else { updateTarget( row.original.targetId, { ...row.original, ip: input } ); } } else { updateTarget(row.original.targetId, { ...row.original, ip: input }); } }} />
{":"}
{ const value = parseInt(e.target.value, 10); if (!isNaN(value) && value > 0) { updateTarget(row.original.targetId, { ...row.original, port: value }); } else { updateTarget(row.original.targetId, { ...row.original, port: 0 }); } }} />
); }, size: 400, minSize: 350, maxSize: 500 }; const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", header: () => ({t("rewritePath")}), cell: ({ row }) => { const hasRewritePath = !!( row.original.rewritePath || row.original.rewritePathType ); const noPathMatch = !row.original.path && !row.original.pathMatchType; return (
{hasRewritePath && !noPathMatch ? ( updateTarget(row.original.targetId, config) } trigger={ } /> ) : ( updateTarget(row.original.targetId, config) } trigger={ } disabled={noPathMatch} /> )}
); }, size: 200, minSize: 180, maxSize: 200 }; const enabledColumn: ColumnDef = { accessorKey: "enabled", header: () => ({t("enabled")}), cell: ({ row }) => (
updateTarget(row.original.targetId, { ...row.original, enabled: val }) } />
), size: 100, minSize: 80, maxSize: 120 }; const actionsColumn: ColumnDef = { id: "actions", header: () => ({t("actions")}), cell: ({ row }) => (
), size: 100, minSize: 80, maxSize: 120 }; if (isAdvancedMode) { const columns = [ addressColumn, healthCheckColumn, enabledColumn, actionsColumn ]; // Only include path-related columns for HTTP resources if (isHttp) { columns.unshift(matchPathColumn); columns.splice(3, 0, rewritePathColumn, priorityColumn); } return columns; } else { return [ addressColumn, healthCheckColumn, enabledColumn, actionsColumn ]; } }; const columns = getColumns(); const table = useReactTable({ data: targets, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), state: { pagination: { pageIndex: 0, pageSize: 1000 } } }); if (pageLoading) { return <>; } return ( {t("targets")} {t("targetsDescription")} {targets.length > 0 ? ( <>
{table .getHeaderGroups() .map((headerGroup) => ( {headerGroup.headers.map( (header) => ( {header.isPlaceholder ? null : flexRender( header .column .columnDef .header, header.getContext() )} ) )} ))} {table.getRowModel().rows?.length ? ( table .getRowModel() .rows.map((row) => ( {row .getVisibleCells() .map((cell) => ( {flexRender( cell .column .columnDef .cell, cell.getContext() )} ))} )) ) : ( {t("targetNoOne")} )} {/* */} {/* {t('targetNoOneDescription')} */} {/* */}
) : (

{t("targetNoOne")}

)}
{resource.http && ( {t("proxyAdditional")} {t("proxyAdditionalDescription")}
{!env.flags.usePangolinDns && ( ( { field.onChange( val ); }} /> )} /> )} ( {t("targetTlsSni")} {t( "targetTlsSniDescription" )} )} />
( { field.onChange(val); }} /> )} />
( {t("proxyCustomHeader")} {t( "proxyCustomHeaderDescription" )} )} /> ( {t("customHeaders")} { field.onChange( value ); }} rows={4} /> {t( "customHeadersDescription" )} )} />
)} {!resource.http && resource.protocol == "tcp" && ( {t("proxyProtocol")} {t("proxyProtocolDescription")}
( { field.onChange(val); }} /> )} /> {proxySettingsForm.watch("proxyProtocol") && ( <> ( {t("proxyProtocolVersion")} {t("versionDescription")} )} /> {t("warning")}: {t("proxyProtocolWarning")} )}
)}
{selectedTargetForHealthCheck && ( { console.log("here"); if (selectedTargetForHealthCheck) { console.log(config); updateTargetHealthCheck( selectedTargetForHealthCheck.targetId, config ); } }} /> )}
); } function isIPInSubnet(subnet: string, ip: string): boolean { const [subnetIP, maskBits] = subnet.split("/"); const mask = parseInt(maskBits); if (mask < 0 || mask > 32) { throw new Error("subnetMaskErrorInvalid"); } // Convert IP addresses to binary numbers const subnetNum = ipToNumber(subnetIP); const ipNum = ipToNumber(ip); // Calculate subnet mask const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1); // Check if the IP is in the subnet return (subnetNum & maskNum) === (ipNum & maskNum); } function ipToNumber(ip: string): number { // Validate IP address format const parts = ip.split("."); if (parts.length !== 4) { throw new Error("ipAddressErrorInvalidFormat"); } // Convert IP octets to 32-bit number return parts.reduce((num, octet) => { const oct = parseInt(octet); if (isNaN(oct) || oct < 0 || oct > 255) { throw new Error("ipAddressErrorInvalidOctet"); } return (num << 8) + oct; }, 0); }