feat: add LocationRulesFields UI component and form wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-28 11:25:57 +01:00
parent 23e186a22e
commit 447dbcedde
3 changed files with 108 additions and 0 deletions

View File

@@ -416,6 +416,18 @@ function parseRedirectsConfig(formData: FormData): RedirectRule[] | null {
}
}
function parseLocationRulesConfig(formData: FormData): import("@/src/lib/models/proxy-hosts").LocationRule[] | null {
const raw = formData.get("location_rules_json");
if (!raw || typeof raw !== "string") return null;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function parseRewriteConfig(formData: FormData): RewriteConfig | null {
const prefix = formData.get("rewrite_path_prefix");
if (!prefix || typeof prefix !== "string" || !prefix.trim()) return null;
@@ -494,6 +506,7 @@ export async function createProxyHostAction(
mtls: parseMtlsConfig(formData),
redirects: parseRedirectsConfig(formData),
rewrite: parseRewriteConfig(formData),
location_rules: parseLocationRulesConfig(formData),
},
userId
);
@@ -570,6 +583,7 @@ export async function updateProxyHostAction(
mtls: formData.has("mtls_present") ? parseMtlsConfig(formData) : undefined,
redirects: formData.has("redirects_json") ? parseRedirectsConfig(formData) : undefined,
rewrite: formData.has("rewrite_path_prefix") ? parseRewriteConfig(formData) : undefined,
location_rules: formData.has("location_rules_json") ? parseLocationRulesConfig(formData) : undefined,
},
userId
);

View File

@@ -25,6 +25,7 @@ import { GeoBlockFields } from "./GeoBlockFields";
import { WafFields } from "./WafFields";
import { MtlsFields } from "./MtlsConfig";
import { RedirectsFields } from "./RedirectsFields";
import { LocationRulesFields } from "./LocationRulesFields";
import { RewriteFields } from "./RewriteFields";
import type { CaCertificate } from "@/lib/models/ca-certificates";
@@ -133,6 +134,7 @@ export function CreateHostDialog({
</Select>
</div>
<RedirectsFields initialData={initialData?.redirects} />
<LocationRulesFields initialData={initialData?.location_rules} />
<RewriteFields initialData={initialData?.rewrite} />
<div>
<label className="text-sm font-medium mb-1 block">Custom Pre-Handlers (JSON)</label>
@@ -263,6 +265,7 @@ export function EditHostDialog({
</Select>
</div>
<RedirectsFields initialData={host.redirects} />
<LocationRulesFields initialData={host.location_rules} />
<RewriteFields initialData={host.rewrite} />
<div>
<label className="text-sm font-medium mb-1 block">Custom Pre-Handlers (JSON)</label>

View File

@@ -0,0 +1,91 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Trash2, Plus } from "lucide-react";
import type { LocationRule } from "@/lib/models/proxy-hosts";
type Props = { initialData?: LocationRule[] };
export function LocationRulesFields({ initialData = [] }: Props) {
const [rules, setRules] = useState<LocationRule[]>(initialData);
const addRule = () =>
setRules((r) => [...r, { path: "", upstreams: [] }]);
const removeRule = (i: number) =>
setRules((r) => r.filter((_, idx) => idx !== i));
const updatePath = (i: number, value: string) =>
setRules((r) => r.map((rule, idx) => (idx === i ? { ...rule, path: value } : rule)));
const updateUpstreams = (i: number, value: string) =>
setRules((r) =>
r.map((rule, idx) =>
idx === i
? {
...rule,
upstreams: value
.split("\n")
.map((u) => u.trim())
.filter(Boolean),
}
: rule
)
);
return (
<div>
<p className="text-sm font-semibold mb-2">Location Rules</p>
<input type="hidden" name="location_rules_json" value={JSON.stringify(rules)} />
{rules.length > 0 && (
<div className="mb-2 flex flex-col gap-3">
{rules.map((rule, i) => (
<div key={i} className="grid grid-cols-[1fr_1fr_40px] gap-2 items-start">
<div>
{i === 0 && (
<span className="text-xs font-medium text-muted-foreground px-1 mb-1 block">Path Pattern</span>
)}
<Input
size={1}
placeholder="/ws/*"
value={rule.path}
onChange={(e) => updatePath(i, e.target.value)}
className="h-8 text-sm"
/>
</div>
<div>
{i === 0 && (
<span className="text-xs font-medium text-muted-foreground px-1 mb-1 block">Upstreams</span>
)}
<Textarea
placeholder={"ws-backend:8080\nws-backend2:8080"}
value={rule.upstreams.join("\n")}
onChange={(e) => updateUpstreams(i, e.target.value)}
className="text-sm min-h-[32px]"
rows={Math.max(1, rule.upstreams.length)}
/>
</div>
<div className={i === 0 ? "mt-5" : ""}>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => removeRule(i)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
<Button type="button" variant="ghost" size="sm" onClick={addRule}>
<Plus className="h-4 w-4 mr-1" />
Add Location Rule
</Button>
</div>
);
}