Add SEED_DEMO_DATA environment variable to control demo data population, enhance setup wizard with welcome screen and theme toggle, add smooth animations for wizard transitions, improve dashboard endpoint to return structured objects for last_syncs and faulty_nodes instead of raw models, implement comprehensive error handling in cluster sync with failed status tracking and audit logging, fix Proxmox provider
122 lines
7.1 KiB
TypeScript
122 lines
7.1 KiB
TypeScript
import { FormEvent, useState } from "react";
|
|
import { CheckCircle2, Moon, Server, ShieldCheck, Sparkles, Sun } from "lucide-react";
|
|
|
|
import { publicApi } from "../api/client";
|
|
import { buttonClass, Field, inputClass, secondaryButtonClass, selectClass } from "../components/FormControls";
|
|
import { useTheme } from "../stores/theme";
|
|
|
|
export function SetupWizard() {
|
|
const [step, setStep] = useState(0);
|
|
const [error, setError] = useState("");
|
|
const { dark, toggle } = useTheme();
|
|
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 overflow-hidden bg-canvas px-4 py-8 text-slate-900 dark:text-slate-100">
|
|
<button
|
|
className="fixed right-4 top-4 z-10 rounded-md border border-border bg-panel p-2 hover:bg-slate-100 dark:hover:bg-slate-800"
|
|
onClick={toggle}
|
|
type="button"
|
|
aria-label="Toggle theme"
|
|
>
|
|
{dark ? <Sun size={18} /> : <Moon size={18} />}
|
|
</button>
|
|
<div className="mx-auto max-w-4xl">
|
|
{step === 0 ? (
|
|
<section className="grid min-h-[78vh] place-items-center text-center">
|
|
<div className="animate-[fadeIn_700ms_ease-out]">
|
|
<div className="mx-auto mb-8 grid h-24 w-24 place-items-center rounded-2xl bg-accent text-white shadow-xl shadow-teal-900/20">
|
|
<Sparkles size={42} />
|
|
</div>
|
|
<h1 className="mb-4 text-5xl font-semibold tracking-normal">Welcome to NexaFabric</h1>
|
|
<p className="mx-auto mb-8 max-w-2xl text-base text-slate-500 dark:text-slate-400">
|
|
A guided setup will create your administrator, connect Proxmox, and start safely in read-only mode.
|
|
</p>
|
|
<button className={buttonClass} type="button" onClick={() => setStep(1)}>Continue</button>
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
{step > 0 ? (
|
|
<>
|
|
<div className="mb-8 flex items-center gap-3 animate-[slideUp_420ms_ease-out]">
|
|
<div className="rounded-md bg-accent p-3 text-white"><Sparkles size={24} /></div>
|
|
<div>
|
|
<h1 className="text-3xl font-semibold">NexaFabric Setup</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 animate-[slideUp_480ms_ease-out]">
|
|
{["Admin", "Provider", "Finish"].map((label, index) => (
|
|
<div key={label} className={`rounded-md border p-3 text-sm ${index + 1 === step ? "border-accent bg-panel" : "border-border bg-panel/60"}`}>
|
|
<div className="font-medium">{label}</div>
|
|
<div className="text-xs text-slate-500">{index + 1 < step ? "Done" : index + 1 === step ? "Current" : "Pending"}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<form onSubmit={complete} className="animate-[slideUp_540ms_ease-out] rounded-md border border-border bg-panel p-5">
|
|
{step === 1 ? (
|
|
<div className="grid gap-4 animate-[fadeIn_300ms_ease-out]">
|
|
<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 === 2 ? (
|
|
<div className="grid gap-4 animate-[fadeIn_300ms_ease-out]">
|
|
<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>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={form.verify_tls} onChange={(event) => setForm({ ...form, verify_tls: event.target.checked })} />
|
|
Verify TLS certificate
|
|
</label>
|
|
</div>
|
|
) : null}
|
|
{step === 3 ? (
|
|
<div className="grid gap-4 animate-[fadeIn_300ms_ease-out]">
|
|
<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))}>Back</button>
|
|
{step < 3 ? <button className={buttonClass} type="button" onClick={() => setStep(step + 1)}>Next</button> : <button className={buttonClass}>Complete Setup</button>}
|
|
</div>
|
|
</form>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|