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:
@@ -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">
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user