From a97b6efe9cc2b6d6095b76929902932692574c8e Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 29 Sep 2025 16:40:37 +0530 Subject: [PATCH] redesign path match and rewrite modal --- .../resources/[niceId]/proxy/page.tsx | 243 ++++++--------- src/components/PathMatchRenameModal.tsx | 293 ++++++++++++++++++ 2 files changed, 385 insertions(+), 151 deletions(-) create mode 100644 src/components/PathMatchRenameModal.tsx diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 23a55494..4442e150 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -73,6 +73,7 @@ import { CircleCheck, CircleX, ArrowRight, + Plus, MoveRight } from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; @@ -95,9 +96,9 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { Badge } from "@app/components/ui/badge"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { HeadersInput } from "@app/components/HeadersInput"; +import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), @@ -597,93 +598,64 @@ export default function ReverseProxyTargets(props: { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { - const [showPathInput, setShowPathInput] = useState( - !!(row.original.path || row.original.pathMatchType) - ); + const hasPathMatch = !!(row.original.path || row.original.pathMatchType); - if (!showPathInput) { - return ( - - ); - } - - return ( -
- - { - const value = e.target.value.trim(); - if (!value) { - setShowPathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - path: null, - pathMatchType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - path: value - }); - } - }} /> - + {/* */}
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + /> ); - } + }, }, { accessorKey: "siteId", @@ -886,98 +858,68 @@ export default function ReverseProxyTargets(props: { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { - const [showRewritePathInput, setShowRewritePathInput] = useState( - !!(row.original.rewritePath || row.original.rewritePathType) - ); + const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType); + const noPathMatch = !row.original.path && !row.original.pathMatchType; - if (!showRewritePathInput) { - const noPathMatch = - !row.original.path && !row.original.pathMatchType; - return ( - - ); - } - - return ( -
+ onChange={(config) => updateTarget(row.original.targetId, config)} + trigger={ + + } + /> - - - - { - const value = e.target.value.trim(); - if (!value) { - setShowRewritePathInput(false); - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: null, - rewritePathType: null - }); - } else { - updateTarget(row.original.targetId, { - ...row.original, - rewritePath: value - }); - } - }} - />
+ ) : ( + updateTarget(row.original.targetId, config)} + trigger={ + + } + disabled={noPathMatch} + /> ); - } + }, }, + // { // accessorKey: "protocol", // header: t('targetProtocol'), @@ -1649,7 +1591,6 @@ export default function ReverseProxyTargets(props: { } function isIPInSubnet(subnet: string, ip: string): boolean { - // Split subnet into IP and mask parts const [subnetIP, maskBits] = subnet.split("/"); const mask = parseInt(maskBits); diff --git a/src/components/PathMatchRenameModal.tsx b/src/components/PathMatchRenameModal.tsx new file mode 100644 index 00000000..574c9c70 --- /dev/null +++ b/src/components/PathMatchRenameModal.tsx @@ -0,0 +1,293 @@ +import { Pencil } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@app/components/ui/dialog"; +import { Badge } from "@app/components/ui/badge"; +import { Label } from "@app/components/ui/label"; +import { useEffect, useState } from "react"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; + + +export function PathMatchModal({ + value, + onChange, + trigger, +}: { + value: { path: string | null; pathMatchType: string | null }; + onChange: (config: { path: string | null; pathMatchType: string | null }) => void; + trigger: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + const [matchType, setMatchType] = useState(value?.pathMatchType || "prefix"); + const [path, setPath] = useState(value?.path || ""); + + useEffect(() => { + if (open) { + setMatchType(value?.pathMatchType || "prefix"); + setPath(value?.path || ""); + } + }, [open, value]); + + const handleSave = () => { + onChange({ pathMatchType: matchType as any, path: path.trim() }); + setOpen(false); + }; + + const handleClear = () => { + onChange({ pathMatchType: null, path: null }); + setOpen(false); + }; + + const getPlaceholder = () => (matchType === "regex" ? "^/api/.*" : "/path"); + + const getHelpText = () => { + switch (matchType) { + case "prefix": + return "Example: /api matches /api, /api/users, etc."; + case "exact": + return "Example: /api matches only /api"; + case "regex": + return "Example: ^/api/.* matches /api/anything"; + default: + return ""; + } + }; + + return ( + + {trigger} + + + Configure Path Matching + + Set up how incoming requests should be matched based on their path. + + +
+
+ + +
+
+ + setPath(e.target.value)} + /> +

{getHelpText()}

+
+
+ + {value?.path && ( + + )} + + +
+
+ ); +} + + +export function PathRewriteModal({ + value, + onChange, + trigger, + disabled, +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; + onChange: (config: { rewritePath: string | null; rewritePathType: string | null }) => void; + trigger: React.ReactNode; + disabled?: boolean; +}) { + const [open, setOpen] = useState(false); + const [rewriteType, setRewriteType] = useState(value?.rewritePathType || "prefix"); + const [rewritePath, setRewritePath] = useState(value?.rewritePath || ""); + + useEffect(() => { + if (open) { + setRewriteType(value?.rewritePathType || "prefix"); + setRewritePath(value?.rewritePath || ""); + } + }, [open, value]); + + const handleSave = () => { + onChange({ rewritePathType: rewriteType as any, rewritePath: rewritePath.trim() }); + setOpen(false); + }; + + const handleClear = () => { + onChange({ rewritePathType: null, rewritePath: null }); + setOpen(false); + }; + + const getPlaceholder = () => { + switch (rewriteType) { + case "regex": + return "/new/$1"; + case "stripPrefix": + return ""; + default: + return "/new-path"; + } + }; + + const getHelpText = () => { + switch (rewriteType) { + case "prefix": + return "Replace the matched prefix with this value"; + case "exact": + return "Replace the entire path with this value"; + case "regex": + return "Use capture groups like $1, $2 for replacement"; + case "stripPrefix": + return "Leave empty to strip prefix or provide new prefix"; + default: + return ""; + } + }; + + return ( + + + {trigger} + + + + Configure Path Rewriting + + Transform the matched path before forwarding to the target. + + +
+
+ + +
+
+ + setRewritePath(e.target.value)} + /> +

{getHelpText()}

+
+
+ + {value?.rewritePath && ( + + )} + + +
+
+ ); +} + +export function PathMatchDisplay({ + value, +}: { + value: { path: string | null; pathMatchType: string | null }; +}) { + if (!value?.path) return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: "Prefix", + exact: "Exact", + regex: "Regex", + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.pathMatchType)} + + + {value.path} + + +
+ ); +} + + +export function PathRewriteDisplay({ + value, +}: { + value: { rewritePath: string | null; rewritePathType: string | null }; +}) { + if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix") return null; + + const getTypeLabel = (type: string | null) => { + const labels: Record = { + prefix: "Prefix", + exact: "Exact", + regex: "Regex", + stripPrefix: "Strip", + }; + return labels[type || ""] || type; + }; + + return ( +
+ + {getTypeLabel(value.rewritePathType)} + + + {value.rewritePath || (strip)} + + +
+ ); +}