feat: add LoadingOverlay component with contextual busy messages for async operations across cluster sync, firewall preview/apply, IPAM discovery/export, node agent installer, and policy compilation
Add LoadingOverlay component with spinner, customizable title/message, and backdrop blur styling, implement busy state tracking with setBusyMessage in Clusters/FirewallPreview/Ipam/Nodes/Policies pages, wrap async operations (cluster test/sync, firewall preview/apply, IPAM discover/export CSV, node agent installer
This commit is contained in:
@@ -5,6 +5,7 @@ import { Database, Download, Plus } from "lucide-react";
|
||||
import { api, authorizedFetch, IpAddress, Network, Subnet } from "../api/client";
|
||||
import { DataTable } from "../components/DataTable";
|
||||
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
|
||||
import { LoadingOverlay } from "../components/LoadingOverlay";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
@@ -18,6 +19,7 @@ export function Ipam() {
|
||||
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 [busyMessage, setBusyMessage] = useState("");
|
||||
|
||||
const createSubnet = useMutation({
|
||||
mutationFn: () => api<Subnet>("/ipam/subnets", { method: "POST", body: JSON.stringify({ ...subnetForm, network_id: subnetForm.network_id || networks.data?.[0]?.id }) }),
|
||||
@@ -49,17 +51,23 @@ export function Ipam() {
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
const response = await authorizedFetch("/ipam/export.csv");
|
||||
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);
|
||||
setBusyMessage("Preparing IPAM export...");
|
||||
try {
|
||||
const response = await authorizedFetch("/ipam/export.csv");
|
||||
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);
|
||||
} finally {
|
||||
setBusyMessage("");
|
||||
}
|
||||
}
|
||||
|
||||
async function discoverIpam() {
|
||||
setBusyMessage("Discovering IP addresses from Proxmox...");
|
||||
try {
|
||||
const result = await api<{ imported: number; removed: number; errors: Array<Record<string, unknown>> }>("/ipam/discover", { method: "POST" });
|
||||
setMessage(`Discovery imported ${result.imported} IP addresses and removed ${result.removed} container bridge entries${result.errors.length ? " with errors" : ""}.`);
|
||||
@@ -67,12 +75,15 @@ export function Ipam() {
|
||||
await queryClient.invalidateQueries({ queryKey: ["addresses"] });
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : "IPAM discovery failed.");
|
||||
} finally {
|
||||
setBusyMessage("");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="IPAM" subtitle="Manage subnets, reservations, assignments, conflicts, and export state." />
|
||||
<LoadingOverlay open={Boolean(busyMessage)} message={busyMessage} />
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className={buttonClass} onClick={() => setSubnetOpen(true)}><Plus size={16} /> Add Subnet</button>
|
||||
|
||||
Reference in New Issue
Block a user