feat: add initial setup wizard, workload insights, and policy audit mode
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user