diff --git a/messages/en-US.json b/messages/en-US.json index f725f853..3cbc4a04 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -469,6 +469,8 @@ "proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.", "proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", "proxyEnableSSL": "Enable SSL (https)", + "target": "Target", + "configureTargets": "Configure Targets", "targetErrorFetch": "Failed to fetch targets", "targetErrorFetchDescription": "An error occurred while fetching targets", "siteErrorFetch": "Failed to fetch resource", diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 302d16d2..028c97c7 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -110,6 +110,8 @@ import { } from "@app/components/PathMatchRenameModal"; import { Badge } from "@app/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { TargetModal } from "@app/components/TargetModal"; +import { TargetDisplay } from "@app/components/TargetDisplay"; const addTargetSchema = z .object({ @@ -537,11 +539,11 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site?.type || null - } + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -552,10 +554,10 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...config, - updated: true - } + ...target, + ...config, + updated: true + } : target ) ); @@ -743,9 +745,9 @@ export default function ReverseProxyTargets(props: { } /> - {/* */} + ) : ( +
+ } + /> + + +
+ ) : ( + updateTarget(row.original.targetId, { ...row.original, - ip: input - }); + ...config + }) } - }} - /> - ) - }, - { - accessorKey: "port", - header: t("targetPort"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) - } - /> - ) + showMethod={resource.http} + trigger={ + + } + /> + ); + } }, { accessorKey: "rewritePath", @@ -990,7 +985,6 @@ export default function ReverseProxyTargets(props: { return hasRewritePath && !noPathMatch ? (
- {/* */} @@ -1318,34 +1312,34 @@ export default function ReverseProxyTargets(props: { ); return selectedSite && selectedSite.type === - "newt" + "newt" ? (() => { - const dockerState = - getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })() + const dockerState = + getDockerStateForSite( + selectedSite.siteId + ); + return ( + + refreshContainersForSite( + selectedSite.siteId + ) + } + /> + ); + })() : null; })()}
@@ -1558,7 +1552,7 @@ export default function ReverseProxyTargets(props: { -
+
{table @@ -1573,12 +1567,12 @@ export default function ReverseProxyTargets(props: { {header.isPlaceholder ? null : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} + header + .column + .columnDef + .header, + header.getContext() + )} ) )} diff --git a/src/components/TargetDisplay.tsx b/src/components/TargetDisplay.tsx new file mode 100644 index 00000000..5ca0a9a7 --- /dev/null +++ b/src/components/TargetDisplay.tsx @@ -0,0 +1,44 @@ +import { Globe, Hash, Shield } from "lucide-react"; + +interface TargetDisplayProps { + value: { + method?: string | null; + ip?: string; + port?: number; + }; + showMethod?: boolean; +} + +export function TargetDisplay({ value, showMethod = true }: TargetDisplayProps) { + const { method, ip, port } = value; + + if (!ip && !port && !method) { + return Not configured; + } + + + + return ( +
+ {showMethod && method && ( + + {method === "https" && } + + {method}:// + + + )} + {ip && ( + + {ip} + {port && :} + + )} + {port && ( + + {port} + + )} +
+ ); +} diff --git a/src/components/TargetModal.tsx b/src/components/TargetModal.tsx new file mode 100644 index 00000000..8a5ec336 --- /dev/null +++ b/src/components/TargetModal.tsx @@ -0,0 +1,144 @@ + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState } from "react"; + +interface TargetConfig { + method?: string | null; + ip?: string; + port?: number; +} + +interface TargetModalProps { + value: TargetConfig; + onChange: (config: TargetConfig) => void; + trigger: React.ReactNode; + showMethod?: boolean; +} + +export function TargetModal({ + value, + onChange, + trigger, + showMethod = true +}: TargetModalProps) { + const [open, setOpen] = useState(false); + const [config, setConfig] = useState(value); + + const handleSave = () => { + onChange(config); + setOpen(false); + }; + + const parseHostTarget = (input: string) => { + const protocolMatch = input.match(/^(https?|h2c):\/\//); + const protocol = protocolMatch ? protocolMatch[1] : null; + const withoutProtocol = input.replace(/^(https?|h2c):\/\//, ''); + + const portMatch = withoutProtocol.match(/:(\d+)(?:\/|$)/); + const port = portMatch ? parseInt(portMatch[1], 10) : null; + const host = withoutProtocol.replace(/:\d+(?:\/|$)/, '').replace(/\/$/, ''); + + return { protocol, host, port }; + }; + + const handleHostChange = (input: string) => { + const trimmed = input.trim(); + const hasProtocol = /^(https?|h2c):\/\//.test(trimmed); + const hasPort = /:\d+(?:\/|$)/.test(trimmed); + + if (hasProtocol || hasPort) { + const parsed = parseHostTarget(trimmed); + setConfig({ + ...config, + ...(hasProtocol && parsed.protocol ? { method: parsed.protocol } : {}), + ip: parsed.host, + ...(hasPort && parsed.port ? { port: parsed.port } : {}) + }); + } else { + setConfig({ ...config, ip: trimmed }); + } + }; + + return ( + + {trigger} + + + Configure Target + +
+ {showMethod && ( +
+ + +
+ )} +
+ + setConfig({ ...config, ip: e.target.value })} + onBlur={(e) => handleHostChange(e.target.value)} + /> +

+ You can also paste: http://example.com:8080 +

+
+
+ + + setConfig({ + ...config, + port: parseInt(e.target.value, 10) || undefined + }) + } + /> +
+
+
+ + +
+
+
+ ); +} \ No newline at end of file