feat: improve setup wizard UX, add cluster sync error handling, and conditional demo data seeding
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
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { CheckCircle2, Server, ShieldCheck, Sparkles } from "lucide-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",
|
||||
@@ -31,34 +33,58 @@ export function SetupWizard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas px-4 py-8 text-slate-900 dark:text-slate-100">
|
||||
<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">
|
||||
<div className="mb-8 flex items-center gap-3">
|
||||
{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">Welcome to NexaFabric</h1>
|
||||
<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">
|
||||
<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 === step ? "border-accent bg-panel" : "border-border bg-panel/60"}`}>
|
||||
<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 < step ? "Done" : index === step ? "Current" : "Pending"}</div>
|
||||
<div className="text-xs text-slate-500">{index + 1 < step ? "Done" : index + 1 === 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">
|
||||
<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 === 1 ? (
|
||||
<div className="grid gap-4">
|
||||
{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>
|
||||
@@ -67,10 +93,14 @@ export function SetupWizard() {
|
||||
<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 === 2 ? (
|
||||
<div className="grid gap-4">
|
||||
{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.
|
||||
@@ -79,10 +109,12 @@ export function SetupWizard() {
|
||||
) : 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>}
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user