mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-03 08:39:09 +00:00
Add prefix to ui and resource
This commit is contained in:
@@ -101,8 +101,42 @@ const addTargetSchema = z.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z.number().int().positive()
|
||||
});
|
||||
siteId: z.number().int().positive(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||
}).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;
|
||||
},
|
||||
{
|
||||
message: "Invalid path configuration"
|
||||
}
|
||||
);
|
||||
|
||||
const targetsSettingsSchema = z.object({
|
||||
stickySession: z.boolean()
|
||||
@@ -221,7 +255,9 @@ export default function ReverseProxyTargets(props: {
|
||||
defaultValues: {
|
||||
ip: "",
|
||||
method: resource.http ? "http" : null,
|
||||
port: "" as any as number
|
||||
port: "" as any as number,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
@@ -396,6 +432,8 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: data.path || null,
|
||||
pathMatchType: data.pathMatchType || null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
@@ -407,7 +445,9 @@ export default function ReverseProxyTargets(props: {
|
||||
addTargetForm.reset({
|
||||
ip: "",
|
||||
method: resource.http ? "http" : null,
|
||||
port: "" as any as number
|
||||
port: "" as any as number,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -450,7 +490,9 @@ export default function ReverseProxyTargets(props: {
|
||||
port: target.port,
|
||||
method: target.method,
|
||||
enabled: target.enabled,
|
||||
siteId: target.siteId
|
||||
siteId: target.siteId,
|
||||
path: target.path,
|
||||
pathMatchType: target.pathMatchType
|
||||
};
|
||||
|
||||
if (target.new) {
|
||||
@@ -720,6 +762,87 @@ export default function ReverseProxyTargets(props: {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "path",
|
||||
header: t("path"),
|
||||
cell: ({ row }) => {
|
||||
const [showPathInput, setShowPathInput] = useState(
|
||||
!!(row.original.path || row.original.pathMatchType)
|
||||
);
|
||||
|
||||
if (!showPathInput) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPathInput(true)}
|
||||
>
|
||||
+ Path
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 min-w-[200px]">
|
||||
<Select
|
||||
defaultValue={row.original.pathMatchType || "exact"}
|
||||
onValueChange={(value) =>
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
pathMatchType: value as "exact" | "prefix" | "regex"
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-25">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="exact">Exact</SelectItem>
|
||||
<SelectItem value="prefix">Prefix</SelectItem>
|
||||
<SelectItem value="regex">Regex</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder={
|
||||
row.original.pathMatchType === "regex"
|
||||
? "^/api/.*"
|
||||
: "/path"
|
||||
}
|
||||
defaultValue={row.original.path || ""}
|
||||
className="flex-1 min-w-[150px]"
|
||||
onBlur={(e) => {
|
||||
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
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowPathInput(false);
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
});
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorKey: "protocol",
|
||||
// header: t('targetProtocol'),
|
||||
|
||||
@@ -112,8 +112,42 @@ const addTargetSchema = z.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z.number().int().positive()
|
||||
});
|
||||
siteId: z.number().int().positive(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||
}).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;
|
||||
},
|
||||
{
|
||||
message: "Invalid path configuration"
|
||||
}
|
||||
);
|
||||
|
||||
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
||||
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
|
||||
@@ -202,7 +236,9 @@ export default function Page() {
|
||||
defaultValues: {
|
||||
ip: "",
|
||||
method: baseForm.watch("http") ? "http" : null,
|
||||
port: "" as any as number
|
||||
port: "" as any as number,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
@@ -273,6 +309,8 @@ export default function Page() {
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: data.path || null,
|
||||
pathMatchType: data.pathMatchType || null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
@@ -284,7 +322,9 @@ export default function Page() {
|
||||
addTargetForm.reset({
|
||||
ip: "",
|
||||
method: baseForm.watch("http") ? "http" : null,
|
||||
port: "" as any as number
|
||||
port: "" as any as number,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,7 +410,9 @@ export default function Page() {
|
||||
port: target.port,
|
||||
method: target.method,
|
||||
enabled: target.enabled,
|
||||
siteId: target.siteId
|
||||
siteId: target.siteId,
|
||||
path: target.path,
|
||||
pathMatchType: target.pathMatchType
|
||||
};
|
||||
|
||||
await api.put(`/resource/${id}/target`, data);
|
||||
@@ -666,6 +708,87 @@ export default function Page() {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "path",
|
||||
header: t("path"),
|
||||
cell: ({ row }) => {
|
||||
const [showPathInput, setShowPathInput] = useState(
|
||||
!!(row.original.path || row.original.pathMatchType)
|
||||
);
|
||||
|
||||
if (!showPathInput) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPathInput(true)}
|
||||
>
|
||||
+ Path
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 min-w-[200px]">
|
||||
<Select
|
||||
defaultValue={row.original.pathMatchType || "exact"}
|
||||
onValueChange={(value) =>
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
pathMatchType: value as "exact" | "prefix" | "regex"
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-25">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="exact">Exact</SelectItem>
|
||||
<SelectItem value="prefix">Prefix</SelectItem>
|
||||
<SelectItem value="regex">Regex</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder={
|
||||
row.original.pathMatchType === "regex"
|
||||
? "^/api/.*"
|
||||
: "/path"
|
||||
}
|
||||
defaultValue={row.original.path || ""}
|
||||
className="flex-1 min-w-[150px]"
|
||||
onBlur={(e) => {
|
||||
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
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowPathInput(false);
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
path: null,
|
||||
pathMatchType: null
|
||||
});
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: t("enabled"),
|
||||
|
||||
Reference in New Issue
Block a user