feat(ui): implement modern NaxOS Web Dashboard with Perses analytics and ZFS migration wizard
CI & Build NaxOS Web Dashboard / build (push) Failing after 1m29s
CI & Build NaxOS Web Dashboard / build (push) Failing after 1m29s
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
HardDrive,
|
||||
Network,
|
||||
UserCheck,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
Database,
|
||||
ShieldCheck,
|
||||
Server,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const WizardView: React.FC = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [storageMode, setStorageMode] = useState<'create' | 'import'>('import');
|
||||
const [poolName, setPoolName] = useState('tank');
|
||||
const [hostname, setHostname] = useState('naxos');
|
||||
const [adminUser, setAdminUser] = useState('admin');
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [gitopsUrl, setGitopsUrl] = useState('ssh://git@git.lholz.de:2222/naxos/naxos-config.git');
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
const handleFinish = () => {
|
||||
setIsDone(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="text-center space-y-1">
|
||||
<h2 className="text-2xl font-bold text-white">NaxOS Appliance Setup Wizard</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
Step-by-step guided configuration for storage layout, network, and security
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="flex items-center justify-between px-6 py-4 rounded-2xl bg-slate-900 border border-slate-800">
|
||||
{[
|
||||
{ num: 1, title: 'Storage & Pools' },
|
||||
{ num: 2, title: 'Network' },
|
||||
{ num: 3, title: 'Credentials' },
|
||||
{ num: 4, title: 'GitOps & Finish' },
|
||||
].map((s) => (
|
||||
<div key={s.num} className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold transition ${
|
||||
step === s.num
|
||||
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/30'
|
||||
: step > s.num
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
: 'bg-slate-800 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{step > s.num ? <CheckCircle2 className="h-4 w-4" /> : s.num}
|
||||
</div>
|
||||
<span className={`text-xs font-medium hidden sm:inline ${step === s.num ? 'text-white' : 'text-slate-500'}`}>
|
||||
{s.title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Form Container */}
|
||||
<div className="p-8 rounded-2xl bg-slate-900 border border-slate-800 space-y-6">
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-emerald-400" />
|
||||
<span>Storage Architecture & ZFS Pool</span>
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStorageMode('import')}
|
||||
className={`p-4 rounded-xl border text-left transition ${
|
||||
storageMode === 'import'
|
||||
? 'bg-emerald-500/10 border-emerald-500 text-white'
|
||||
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-xs">Safe Import Existing Pool</div>
|
||||
<div className="text-[11px] text-slate-400 mt-1">
|
||||
Import TrueNAS or nixos-lukas pool without formatting. Preserves Immich photo library.
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStorageMode('create')}
|
||||
className={`p-4 rounded-xl border text-left transition ${
|
||||
storageMode === 'create'
|
||||
? 'bg-emerald-500/10 border-emerald-500 text-white'
|
||||
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-xs">Create New Pool</div>
|
||||
<div className="text-[11px] text-slate-400 mt-1">
|
||||
Initialize fresh ZFS mirror or RAID-Z pool across raw disks with ashift=12.
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1">Target Pool Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={poolName}
|
||||
onChange={(e) => setPoolName(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Network className="h-4 w-4 text-emerald-400" />
|
||||
<span>Network Configuration</span>
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1">Appliance Hostname</label>
|
||||
<input
|
||||
type="text"
|
||||
value={hostname}
|
||||
onChange={(e) => setHostname(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-400">
|
||||
DHCP will automatically request an IP address and advertise mDNS name <strong className="text-white font-mono">{hostname}.local</strong> via Avahi.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<UserCheck className="h-4 w-4 text-emerald-400" />
|
||||
<span>Administrator Credentials</span>
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1">Admin Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={adminUser}
|
||||
onChange={(e) => setAdminUser(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1">Admin Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={adminPassword}
|
||||
onChange={(e) => setAdminPassword(e.target.value)}
|
||||
placeholder="••••••••••••"
|
||||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-emerald-400" />
|
||||
<span>GitOps Remote & Final Validation</span>
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-400 mb-1">Remote Git Repository (Gitea)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={gitopsUrl}
|
||||
onChange={(e) => setGitopsUrl(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono text-xs focus:outline-none focus:border-emerald-500"
|
||||
/>
|
||||
<p className="text-[11px] text-slate-500 mt-1">
|
||||
All future web dashboard configuration adjustments will be committed here.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isDone && (
|
||||
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center gap-3 text-emerald-300 text-xs">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0" />
|
||||
<span>NaxOS Appliance successfully initialized and ready for production!</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-between pt-4 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
disabled={step === 1}
|
||||
onClick={() => setStep(step - 1)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 disabled:opacity-30 text-slate-300 text-xs font-medium"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
|
||||
{step < 4 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(step + 1)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold shadow-sm"
|
||||
>
|
||||
<span>Next Step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFinish}
|
||||
className="flex items-center gap-1.5 px-5 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold shadow-sm"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<span>Complete Setup</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user