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:
@@ -0,0 +1,89 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { CheckCircle2, Server, ShieldCheck, Sparkles } from "lucide-react";
|
||||
|
||||
import { publicApi } from "../api/client";
|
||||
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
|
||||
|
||||
export function SetupWizard() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [error, setError] = useState("");
|
||||
const [form, setForm] = useState({
|
||||
admin_email: "admin@nexafabric.local",
|
||||
admin_name: "NexaFabric Administrator",
|
||||
admin_password: "ChangeMe_UseEnvInstead",
|
||||
cluster_name: "Production Proxmox",
|
||||
cluster_api_url: "https://pve.example.local:8006",
|
||||
cluster_api_token: "",
|
||||
cluster_provider: "proxmox",
|
||||
cluster_mode: "read_only",
|
||||
verify_tls: true,
|
||||
});
|
||||
|
||||
async function complete(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setError("");
|
||||
try {
|
||||
await publicApi("/setup/complete", { method: "POST", body: JSON.stringify(form) });
|
||||
window.location.href = "/login";
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Setup failed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas px-4 py-8 text-slate-900 dark:text-slate-100">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="mb-8 flex items-center gap-3">
|
||||
<div className="rounded-md bg-accent p-3 text-white"><Sparkles size={24} /></div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Welcome to NexaFabric</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">Create your first administrator and connect your first provider.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 grid gap-3 md:grid-cols-3">
|
||||
{["Admin", "Provider", "Finish"].map((label, index) => (
|
||||
<div key={label} className={`rounded-md border p-3 text-sm ${index === step ? "border-accent bg-panel" : "border-border bg-panel/60"}`}>
|
||||
<div className="font-medium">{label}</div>
|
||||
<div className="text-xs text-slate-500">{index < step ? "Done" : index === step ? "Current" : "Pending"}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={complete} className="rounded-md border border-border bg-panel p-5">
|
||||
{step === 0 ? (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-2 font-medium"><ShieldCheck size={18} /> Super Admin</div>
|
||||
<Field label="Email"><input className={inputClass} value={form.admin_email} onChange={(event) => setForm({ ...form, admin_email: event.target.value })} /></Field>
|
||||
<Field label="Name"><input className={inputClass} value={form.admin_name} onChange={(event) => setForm({ ...form, admin_name: event.target.value })} /></Field>
|
||||
<Field label="Password"><input className={inputClass} type="password" value={form.admin_password} onChange={(event) => setForm({ ...form, admin_password: event.target.value })} /></Field>
|
||||
</div>
|
||||
) : null}
|
||||
{step === 1 ? (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-2 font-medium"><Server size={18} /> First Provider</div>
|
||||
<Field label="Cluster Name"><input className={inputClass} value={form.cluster_name} onChange={(event) => setForm({ ...form, cluster_name: event.target.value })} /></Field>
|
||||
<Field label="API URL"><input className={inputClass} value={form.cluster_api_url} onChange={(event) => setForm({ ...form, cluster_api_url: event.target.value })} /></Field>
|
||||
<Field label="API Token"><input className={inputClass} value={form.cluster_api_token} onChange={(event) => setForm({ ...form, cluster_api_token: event.target.value })} placeholder="PVEAPIToken=..." /></Field>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Field label="Provider"><select className={selectClass} value={form.cluster_provider} onChange={(event) => setForm({ ...form, cluster_provider: event.target.value })}><option>proxmox</option><option>demo</option></select></Field>
|
||||
<Field label="Mode"><select className={selectClass} value={form.cluster_mode} onChange={(event) => setForm({ ...form, cluster_mode: event.target.value })}><option value="read_only">read_only</option><option value="write_enabled">write_enabled</option></select></Field>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{step === 2 ? (
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-2 font-medium"><CheckCircle2 size={18} /> Ready</div>
|
||||
<div className="rounded-md border border-border bg-canvas p-4 text-sm">
|
||||
NexaFabric will create the administrator, store the provider in read-only mode by default, and open the login screen.
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{error ? <div className="mt-4 rounded-md border border-danger p-3 text-sm text-danger">{error}</div> : null}
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button className={secondaryButtonClass} type="button" onClick={() => setStep(Math.max(0, step - 1))} disabled={step === 0}>Back</button>
|
||||
{step < 2 ? <button className={buttonClass} type="button" onClick={() => setStep(step + 1)}>Next</button> : <button className={buttonClass}>Complete Setup</button>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user