feat: add comprehensive CRUD endpoints, cluster sync improvements, and firewall orchestration
CI / backend (push) Failing after 2s
CI / frontend (push) Failing after 30s

Add create endpoints for users, roles, tenants, projects, networks, subnets, and security rules with audit logging, implement commit_or_400 helper for IntegrityError handling with 409 responses, enhance cluster sync to populate nodes, workloads, and networks from provider inventory with last_sync_at tracking, add update/delete operations for IP addresses and policies with version tracking, implement IP
This commit is contained in:
2026-07-09 12:33:36 +02:00
parent dea27b5459
commit a911d36f34
15 changed files with 1267 additions and 36 deletions
+112
View File
@@ -0,0 +1,112 @@
import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { GitBranch, Play, Plus } from "lucide-react";
import { api, Policy, Project, ServiceCatalogItem } from "../api/client";
import { DataTable } from "../components/DataTable";
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
import { PageHeader } from "../components/PageHeader";
export function Policies() {
const queryClient = useQueryClient();
const policies = useQuery({ queryKey: ["policies"], queryFn: () => api<Policy[]>("/policies") });
const projects = useQuery({ queryKey: ["projects"], queryFn: () => api<Project[]>("/projects") });
const services = useQuery({ queryKey: ["service-catalog"], queryFn: () => api<ServiceCatalogItem[]>("/service-catalog") });
const [preview, setPreview] = useState("");
const [form, setForm] = useState({
project_id: "",
name: "Web to DB",
source: "sg:Web Tier",
destination: "sg:Database",
service_id: "",
protocol: "tcp",
ports: "5432",
action: "allow",
direction: "egress",
logging: true,
description: "Allow application database traffic",
});
const create = useMutation({
mutationFn: () => {
const service = services.data?.find((item) => item.id === form.service_id);
return api<Policy>("/policies", {
method: "POST",
body: JSON.stringify({
project_id: form.project_id || null,
name: form.name,
enabled: true,
definition: {
source: form.source,
destination: form.destination,
service: { protocol: service?.protocol ?? form.protocol, ports: service?.ports ?? form.ports },
action: form.action,
direction: form.direction,
logging: form.logging,
description: form.description,
},
}),
});
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["policies"] }),
});
async function submit(event: FormEvent) {
event.preventDefault();
await create.mutateAsync();
}
async function compile(policy: Policy) {
const data = await api<Policy>(`/policies/${policy.id}/compile`, { method: "POST" });
setPreview(JSON.stringify(data.last_compiled, null, 2));
await queryClient.invalidateQueries({ queryKey: ["policies"] });
}
async function firewallPreview(policy: Policy) {
const data = await api<Record<string, unknown>>(`/firewall/preview/${policy.id}`, { method: "POST" });
setPreview(JSON.stringify(data, null, 2));
}
return (
<>
<PageHeader title="Policies" subtitle="Create versioned microsegmentation policies and compile firewall previews." />
<div className="grid gap-4 xl:grid-cols-[420px_1fr]">
<form onSubmit={submit} className="rounded-md border border-border bg-panel p-4">
<div className="mb-4 flex items-center gap-2 font-medium"><GitBranch size={18} /> Add Policy</div>
<div className="grid gap-3">
<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>
<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="Destination"><input className={inputClass} value={form.destination} onChange={(event) => setForm({ ...form, destination: event.target.value })} /></Field>
</div>
<Field label="Service"><select className={selectClass} value={form.service_id} onChange={(event) => setForm({ ...form, service_id: 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">
<Field label="Protocol"><input className={inputClass} value={form.protocol} onChange={(event) => setForm({ ...form, protocol: event.target.value })} /></Field>
<Field label="Ports"><input className={inputClass} value={form.ports} onChange={(event) => setForm({ ...form, ports: event.target.value })} /></Field>
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="Action"><select className={selectClass} value={form.action} onChange={(event) => setForm({ ...form, action: event.target.value })}><option>allow</option><option>deny</option><option>reject</option></select></Field>
<Field label="Direction"><select className={selectClass} value={form.direction} onChange={(event) => setForm({ ...form, direction: event.target.value })}><option>ingress</option><option>egress</option></select></Field>
</div>
<Field label="Description"><input className={inputClass} value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Field>
<button className={buttonClass}><Plus size={16} /> Save Policy</button>
</div>
</form>
<section className="space-y-4">
<DataTable rows={(policies.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "name", label: "Policy" }, { key: "version", label: "Version" }, { key: "enabled", label: "Enabled" }]} />
<div className="flex flex-wrap gap-2">
{(policies.data ?? []).map((policy) => (
<div key={policy.id} className="flex gap-2">
<button className={secondaryButtonClass} onClick={() => compile(policy)}><Play size={16} /> Compile {policy.name}</button>
<button className={secondaryButtonClass} onClick={() => firewallPreview(policy)}><Play size={16} /> Preview</button>
</div>
))}
</div>
<pre className="min-h-40 overflow-auto rounded-md border border-border bg-panel p-3 text-xs">{preview || "No policy output yet."}</pre>
</section>
</div>
</>
);
}