feat: add initial setup wizard, workload insights, and policy audit mode
CI / backend (push) Failing after 3s
CI / frontend (push) Failing after 29s

Add setup wizard with status tracking via SystemSetting model, implement /setup/status and /setup/complete endpoints to create initial admin user and optional cluster configuration, add workload insights endpoint with traffic analysis and policy matching including audit mode detection, implement enforcement_mode property on Policy model with audit/enforced states, add Modal component for dialogs, create SetupWizard page with multi
This commit is contained in:
2026-07-09 12:47:08 +02:00
parent a911d36f34
commit 3cd2c0a2f1
18 changed files with 600 additions and 79 deletions
+25 -8
View File
@@ -5,6 +5,7 @@ import { Database, Download, Plus } from "lucide-react";
import { api, IpAddress, Network, Subnet, token } from "../api/client";
import { DataTable } from "../components/DataTable";
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
import { Modal } from "../components/Modal";
import { PageHeader } from "../components/PageHeader";
export function Ipam() {
@@ -14,14 +15,22 @@ export function Ipam() {
const addresses = useQuery({ queryKey: ["addresses"], queryFn: () => api<IpAddress[]>("/ipam/addresses") });
const [subnetForm, setSubnetForm] = useState({ network_id: "", cidr: "10.50.0.0/24", gateway: "10.50.0.1", dns: ["10.50.0.10"], dhcp_enabled: false });
const [ipForm, setIpForm] = useState({ subnet_id: "", address: "10.50.0.20", status: "reserved", note: "" });
const [subnetOpen, setSubnetOpen] = useState(false);
const [ipOpen, setIpOpen] = useState(false);
const createSubnet = useMutation({
mutationFn: () => api<Subnet>("/ipam/subnets", { method: "POST", body: JSON.stringify({ ...subnetForm, network_id: subnetForm.network_id || networks.data?.[0]?.id }) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["subnets"] }),
onSuccess: () => {
setSubnetOpen(false);
queryClient.invalidateQueries({ queryKey: ["subnets"] });
},
});
const createIp = useMutation({
mutationFn: () => api<IpAddress>("/ipam/addresses", { method: "POST", body: JSON.stringify({ ...ipForm, subnet_id: ipForm.subnet_id || subnets.data?.[0]?.id }) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["addresses"] }),
onSuccess: () => {
setIpOpen(false);
queryClient.invalidateQueries({ queryKey: ["addresses"] });
},
});
async function submitSubnet(event: FormEvent) {
@@ -50,8 +59,14 @@ export function Ipam() {
return (
<>
<PageHeader title="IPAM" subtitle="Manage subnets, reservations, assignments, conflicts, and export state." />
<div className="grid gap-4 xl:grid-cols-[360px_360px_1fr]">
<form onSubmit={submitSubnet} className="rounded-md border border-border bg-panel p-4">
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<button className={buttonClass} onClick={() => setSubnetOpen(true)}><Plus size={16} /> Add Subnet</button>
<button className={buttonClass} onClick={() => setIpOpen(true)}><Plus size={16} /> Reserve IP</button>
<button className={secondaryButtonClass} onClick={exportCsv}><Download size={16} /> Export CSV</button>
</div>
<Modal title="Add Subnet" open={subnetOpen} onClose={() => setSubnetOpen(false)}>
<form onSubmit={submitSubnet}>
<div className="mb-4 flex items-center gap-2 font-medium"><Database size={18} /> Add Subnet</div>
<div className="grid gap-3">
<Field label="Network">
@@ -64,8 +79,10 @@ export function Ipam() {
<Field label="Gateway"><input className={inputClass} value={subnetForm.gateway} onChange={(event) => setSubnetForm({ ...subnetForm, gateway: event.target.value })} /></Field>
<button className={buttonClass}><Plus size={16} /> Add Subnet</button>
</div>
</form>
<form onSubmit={submitIp} className="rounded-md border border-border bg-panel p-4">
</form>
</Modal>
<Modal title="Reserve IP" open={ipOpen} onClose={() => setIpOpen(false)}>
<form onSubmit={submitIp}>
<div className="mb-4 flex items-center gap-2 font-medium"><Plus size={18} /> Reserve IP</div>
<div className="grid gap-3">
<Field label="Subnet">
@@ -83,9 +100,9 @@ export function Ipam() {
<Field label="Note"><input className={inputClass} value={ipForm.note} onChange={(event) => setIpForm({ ...ipForm, note: event.target.value })} /></Field>
<button className={buttonClass}><Plus size={16} /> Save IP</button>
</div>
</form>
</form>
</Modal>
<section className="space-y-4">
<button className={secondaryButtonClass} onClick={exportCsv}><Download size={16} /> Export CSV</button>
<DataTable rows={(addresses.data ?? []) as unknown as Record<string, unknown>[]} columns={[{ key: "address", label: "Address" }, { key: "status", label: "Status" }, { key: "note", label: "Note" }]} />
</section>
</div>