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
69 lines
3.2 KiB
TypeScript
69 lines
3.2 KiB
TypeScript
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { Play, ShieldCheck } from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
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 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="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>
|
|
</>
|
|
);
|
|
}
|