import { FormEvent, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Save, Wand2 } from "lucide-react"; import { api, Network, Policy, SecurityGroup, ServiceCatalogItem, Workload } from "../api/client"; import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls"; import { PageHeader } from "../components/PageHeader"; type TargetOption = { label: string; value: string; }; export function PolicyDesigner() { const queryClient = useQueryClient(); const workloads = useQuery({ queryKey: ["workloads"], queryFn: () => api("/vms") }); const securityGroups = useQuery({ queryKey: ["security-groups"], queryFn: () => api("/security-groups") }); const networks = useQuery({ queryKey: ["networks"], queryFn: () => api("/networks") }); const services = useQuery({ queryKey: ["service-catalog"], queryFn: () => api("/service-catalog") }); const [preview, setPreview] = useState | null>(null); const [form, setForm] = useState({ name: "", source: "any", destination: "any", service_id: "", protocol: "tcp", ports: "443", action: "allow", direction: "ingress", enforcement_mode: "enforced", logging: true, description: "Policy intent", }); const targets = useMemo(() => { return [ { label: "Any", value: "any" }, ...(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}` })), ...(networks.data ?? []).map((network) => ({ label: `Network: ${network.name}`, value: `network:${network.name}` })), ]; }, [networks.data, securityGroups.data, workloads.data]); function payload() { return { project_id: null, name: form.name.trim(), enabled: true, definition: { source: form.source, destination: form.destination, service: { protocol: form.protocol, ports: form.ports }, action: form.action, direction: form.direction, enforcement_mode: form.enforcement_mode, logging: form.logging, description: form.description, }, }; } const save = useMutation({ mutationFn: () => api("/policies", { method: "POST", body: JSON.stringify(payload()) }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policies"] }), }); async function submit(event: FormEvent) { event.preventDefault(); await save.mutateAsync(); } function dryRun() { const current = payload(); const warnings = []; if (current.definition.source === "any" && current.definition.destination === "any") { warnings.push("Policy targets all sources and destinations."); } if (current.definition.enforcement_mode === "audit") { warnings.push("Audit mode logs decisions without enforcing them."); } setPreview({ affected_workloads: (workloads.data ?? []).filter((workload) => [current.definition.source, current.definition.destination].includes(`workload:${workload.id}`), ), generated_rule: current.definition, conflicts: [], warnings, }); } function chooseService(serviceId: string) { const service = services.data?.find((item) => item.id === serviceId); setForm({ ...form, service_id: serviceId, protocol: service?.protocol ?? form.protocol, ports: service?.ports ?? form.ports, }); } return ( <>
setForm({ ...form, name: event.target.value })} placeholder="DNS from VMs to resolver" required />
setForm({ ...form, ports: event.target.value })} />
setForm({ ...form, description: event.target.value })} />
); }