feat: add custom IP/CIDR input option to policy source and destination fields with dynamic text input and placeholder examples

Add customTargetValue constant for custom IP/CIDR selection, implement endpointSelectValue helper to detect custom values not in target list, add isCustomEndpoint predicate to show/hide custom input fields, extend PolicyDesigner with "Custom IP/CIDR" dropdown option that reveals text input for manual IP/CIDR entry with trim on change, add placeholder examples "172.16.0.50 or 172.16.0.0/16" to guide user
This commit is contained in:
2026-07-09 20:19:52 +02:00
parent 1802c2cbee
commit 2f7da934ea
2 changed files with 41 additions and 4 deletions
+2 -2
View File
@@ -216,8 +216,8 @@ export function Policies() {
<Field label="Project"><select className={selectClass} value={form.project_id} onChange={(event) => setForm({ ...form, project_id: event.target.value })}><option value="">Global</option>{(projects.data ?? []).map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}</select></Field> <Field label="Project"><select className={selectClass} value={form.project_id} onChange={(event) => setForm({ ...form, project_id: event.target.value })}><option value="">Global</option>{(projects.data ?? []).map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}</select></Field>
<Field label="Name"><input className={inputClass} value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} /></Field> <Field label="Name"><input className={inputClass} value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} /></Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Field label="Source"><input className={inputClass} value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })} /></Field> <Field label="Source"><input className={inputClass} value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })} placeholder="any, workload:<id>, 172.16.0.50 or 172.16.0.0/16" /></Field>
<Field label="Destination"><input className={inputClass} value={form.destination} onChange={(event) => setForm({ ...form, destination: event.target.value })} /></Field> <Field label="Destination"><input className={inputClass} value={form.destination} onChange={(event) => setForm({ ...form, destination: event.target.value })} placeholder="any, workload:<id>, 172.16.0.53 or 172.16.10.0/24" /></Field>
</div> </div>
<Field label="Service"><select className={selectClass} value={form.service_id} onChange={(event) => chooseService(event.target.value)}><option value="">Custom</option>{(services.data ?? []).map((service) => <option key={service.id} value={service.id}>{service.name} {service.ports}</option>)}</select></Field> <Field label="Service"><select className={selectClass} value={form.service_id} onChange={(event) => chooseService(event.target.value)}><option value="">Custom</option>{(services.data ?? []).map((service) => <option key={service.id} value={service.id}>{service.name} {service.ports}</option>)}</select></Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
+39 -2
View File
@@ -11,6 +11,16 @@ type TargetOption = {
value: string; value: string;
}; };
const customTargetValue = "__custom_ip_cidr__";
function endpointSelectValue(value: string, targets: TargetOption[]) {
return targets.some((target) => target.value === value) ? value : customTargetValue;
}
function isCustomEndpoint(value: string, targets: TargetOption[]) {
return endpointSelectValue(value, targets) === customTargetValue;
}
export function PolicyDesigner() { export function PolicyDesigner() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const workloads = useQuery({ queryKey: ["workloads"], queryFn: () => api<Workload[]>("/vms") }); const workloads = useQuery({ queryKey: ["workloads"], queryFn: () => api<Workload[]>("/vms") });
@@ -35,6 +45,7 @@ export function PolicyDesigner() {
const targets = useMemo<TargetOption[]>(() => { const targets = useMemo<TargetOption[]>(() => {
return [ return [
{ label: "Any", value: "any" }, { label: "Any", value: "any" },
{ label: "Custom IP/CIDR", value: customTargetValue },
...(workloads.data ?? []).map((workload) => ({ label: `VM/LXC: ${workload.name}`, value: `workload:${workload.id}` })), ...(workloads.data ?? []).map((workload) => ({ label: `VM/LXC: ${workload.name}`, value: `workload:${workload.id}` })),
...(securityGroups.data ?? []).map((group) => ({ label: `Security Group: ${group.name}`, value: `sg:${group.name}` })), ...(securityGroups.data ?? []).map((group) => ({ label: `Security Group: ${group.name}`, value: `sg:${group.name}` })),
...(networks.data ?? []).map((network) => ({ label: `Network: ${network.name}`, value: `network:${network.name}` })), ...(networks.data ?? []).map((network) => ({ label: `Network: ${network.name}`, value: `network:${network.name}` })),
@@ -115,14 +126,40 @@ export function PolicyDesigner() {
</Field> </Field>
<div /> <div />
<Field label="Source"> <Field label="Source">
<select className={selectClass} value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}> <select
className={selectClass}
value={endpointSelectValue(form.source, targets)}
onChange={(event) => setForm({ ...form, source: event.target.value === customTargetValue ? "" : event.target.value })}
>
{targets.map((target) => <option key={target.value} value={target.value}>{target.label}</option>)} {targets.map((target) => <option key={target.value} value={target.value}>{target.label}</option>)}
</select> </select>
{isCustomEndpoint(form.source, targets) ? (
<input
className={`${inputClass} mt-2`}
value={form.source}
onChange={(event) => setForm({ ...form, source: event.target.value.trim() })}
placeholder="172.16.0.50 or 172.16.0.0/16"
required
/>
) : null}
</Field> </Field>
<Field label="Destination"> <Field label="Destination">
<select className={selectClass} value={form.destination} onChange={(event) => setForm({ ...form, destination: event.target.value })}> <select
className={selectClass}
value={endpointSelectValue(form.destination, targets)}
onChange={(event) => setForm({ ...form, destination: event.target.value === customTargetValue ? "" : event.target.value })}
>
{targets.map((target) => <option key={target.value} value={target.value}>{target.label}</option>)} {targets.map((target) => <option key={target.value} value={target.value}>{target.label}</option>)}
</select> </select>
{isCustomEndpoint(form.destination, targets) ? (
<input
className={`${inputClass} mt-2`}
value={form.destination}
onChange={(event) => setForm({ ...form, destination: event.target.value.trim() })}
placeholder="172.16.0.53 or 172.16.10.0/24"
required
/>
) : null}
</Field> </Field>
<Field label="Service"> <Field label="Service">
<select className={selectClass} value={form.service_id} onChange={(event) => chooseService(event.target.value)}> <select className={selectClass} value={form.service_id} onChange={(event) => chooseService(event.target.value)}>