feat: add comprehensive CRUD endpoints, cluster sync improvements, and firewall orchestration
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:
@@ -0,0 +1,94 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
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 { PageHeader } from "../components/PageHeader";
|
||||
|
||||
export function Ipam() {
|
||||
const queryClient = useQueryClient();
|
||||
const networks = useQuery({ queryKey: ["networks"], queryFn: () => api<Network[]>("/networks") });
|
||||
const subnets = useQuery({ queryKey: ["subnets"], queryFn: () => api<Subnet[]>("/ipam/subnets") });
|
||||
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 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"] }),
|
||||
});
|
||||
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"] }),
|
||||
});
|
||||
|
||||
async function submitSubnet(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
await createSubnet.mutateAsync();
|
||||
}
|
||||
|
||||
async function submitIp(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
await createIp.mutateAsync();
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
const response = await fetch("/api/v1/ipam/export.csv", {
|
||||
headers: token() ? { Authorization: `Bearer ${token()}` } : {},
|
||||
});
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "nexafabric-ipam.csv";
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
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="mb-4 flex items-center gap-2 font-medium"><Database size={18} /> Add Subnet</div>
|
||||
<div className="grid gap-3">
|
||||
<Field label="Network">
|
||||
<select className={selectClass} value={subnetForm.network_id} onChange={(event) => setSubnetForm({ ...subnetForm, network_id: event.target.value })}>
|
||||
<option value="">Auto select</option>
|
||||
{(networks.data ?? []).map((network) => <option key={network.id} value={network.id}>{network.name}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="CIDR"><input className={inputClass} value={subnetForm.cidr} onChange={(event) => setSubnetForm({ ...subnetForm, cidr: event.target.value })} /></Field>
|
||||
<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">
|
||||
<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">
|
||||
<select className={selectClass} value={ipForm.subnet_id} onChange={(event) => setIpForm({ ...ipForm, subnet_id: event.target.value })}>
|
||||
<option value="">Auto select</option>
|
||||
{(subnets.data ?? []).map((subnet) => <option key={subnet.id} value={subnet.id}>{subnet.cidr}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Address"><input className={inputClass} value={ipForm.address} onChange={(event) => setIpForm({ ...ipForm, address: event.target.value })} /></Field>
|
||||
<Field label="Status">
|
||||
<select className={selectClass} value={ipForm.status} onChange={(event) => setIpForm({ ...ipForm, status: event.target.value })}>
|
||||
{["free", "reserved", "assigned", "deprecated", "conflict"].map((status) => <option key={status}>{status}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<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>
|
||||
<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