235 lines
9.5 KiB
TypeScript
235 lines
9.5 KiB
TypeScript
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>
|
|
);
|
|
};
|