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
+50 -15
View File
@@ -1,33 +1,68 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { Play } from "lucide-react";
import { Play, ShieldCheck } from "lucide-react";
import { useState } from "react";
import { api, Policy } from "../api/client";
import { api, Cluster, Policy } from "../api/client";
import { buttonClass, Field, secondaryButtonClass, selectClass } from "../components/FormControls";
import { PageHeader } from "../components/PageHeader";
export function FirewallPreview() {
const policies = useQuery({ queryKey: ["policies"], queryFn: () => api<Policy[]>("/policies") });
const clusters = useQuery({ queryKey: ["clusters"], queryFn: () => api<Cluster[]>("/clusters") });
const [policyId, setPolicyId] = useState("");
const [clusterId, setClusterId] = useState("");
const [dryRun, setDryRun] = useState(true);
const preview = useMutation({
mutationFn: (policyId: string) => api<Record<string, unknown>>(`/firewall/preview/${policyId}`, { method: "POST" }),
});
const firstPolicy = policies.data?.[0];
const apply = useMutation({
mutationFn: () =>
api<Record<string, unknown>>("/firewall/apply", {
method: "POST",
body: JSON.stringify({
policy_id: policyId || policies.data?.[0]?.id,
cluster_id: clusterId || clusters.data?.[0]?.id,
confirm: true,
dry_run: dryRun,
}),
}),
});
const selectedPolicyId = policyId || policies.data?.[0]?.id || "";
return (
<>
<PageHeader title="Firewall Preview" subtitle="Compile policies into provider-specific rules before any apply operation." />
<div className="rounded-md border border-border bg-panel p-4">
<button
className="inline-flex h-10 items-center gap-2 rounded-md bg-accent px-4 text-sm text-white disabled:opacity-50"
disabled={!firstPolicy}
onClick={() => firstPolicy && preview.mutate(firstPolicy.id)}
>
<Play size={18} />
Generate Preview
</button>
<pre className="mt-4 max-h-[520px] overflow-auto rounded-md border border-border bg-canvas p-4 text-xs">
{preview.data ? JSON.stringify(preview.data, null, 2) : "No preview generated yet."}
<div className="grid gap-4 lg:grid-cols-[360px_1fr]">
<section className="rounded-md border border-border bg-panel p-4">
<div className="grid gap-3">
<Field label="Policy">
<select className={selectClass} value={selectedPolicyId} onChange={(event) => setPolicyId(event.target.value)}>
{(policies.data ?? []).map((policy) => <option key={policy.id} value={policy.id}>{policy.name}</option>)}
</select>
</Field>
<Field label="Cluster">
<select className={selectClass} value={clusterId || clusters.data?.[0]?.id || ""} onChange={(event) => setClusterId(event.target.value)}>
{(clusters.data ?? []).map((cluster) => <option key={cluster.id} value={cluster.id}>{cluster.name}</option>)}
</select>
</Field>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={dryRun} onChange={(event) => setDryRun(event.target.checked)} />
Dry run
</label>
<button className={secondaryButtonClass} disabled={!selectedPolicyId} onClick={() => preview.mutate(selectedPolicyId)}>
<Play size={18} />
Generate Preview
</button>
<button className={buttonClass} disabled={!selectedPolicyId || apply.isPending} onClick={() => apply.mutate()}>
<ShieldCheck size={18} />
Apply Confirmed
</button>
</div>
</section>
<pre className="max-h-[620px] overflow-auto rounded-md border border-border bg-panel p-4 text-xs">
{apply.data ? JSON.stringify(apply.data, null, 2) : preview.data ? JSON.stringify(preview.data, null, 2) : "No firewall output yet."}
</pre>
</div>
</>
);
}