Files
HarborForge.Frontend/src/pages/SetupWizardPage.tsx

218 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import axios from 'axios'
interface Props {
wizardBase: string
onComplete: () => void
}
interface SetupForm {
admin_username: string
admin_password: string
admin_email: string
admin_full_name: string
db_host: string
db_port: number
db_user: string
db_password: string
db_database: string
backend_base_url: string
project_name: string
project_description: string
}
const STEPS = ['Welcome', 'Database', 'Admin', 'Backend', 'Finish']
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
const [step, setStep] = useState(0)
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
const [wizardOk, setWizardOk] = useState<boolean | null>(null)
const [form, setForm] = useState<SetupForm>({
admin_username: 'admin',
admin_password: '',
admin_email: '',
admin_full_name: 'Admin',
db_host: 'mysql',
db_port: 3306,
db_user: 'harborforge',
db_password: 'harborforge_pass',
db_database: 'harborforge',
backend_base_url: 'http://backend:8000',
project_name: '',
project_description: '',
})
const wizardApi = axios.create({
baseURL: wizardBase,
timeout: 5000,
})
const set = (key: keyof SetupForm, value: string | number) =>
setForm((f) => ({ ...f, [key]: value }))
const checkWizard = async () => {
setError('')
try {
await wizardApi.get('/health')
setWizardOk(true)
setStep(1)
} catch {
setWizardOk(false)
setError(`Unable to connect to AbstractWizard (${wizardBase}).\nPlease ensure the SSH tunnel is configured:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
}
}
const saveConfig = async () => {
setError('')
setSaving(true)
try {
const config = {
initialized: true,
admin: {
username: form.admin_username,
password: form.admin_password,
email: form.admin_email,
full_name: form.admin_full_name,
},
database: {
host: form.db_host,
port: form.db_port,
user: form.db_user,
password: form.db_password,
database: form.db_database,
},
backend_url: form.backend_base_url || undefined,
}
await wizardApi.put('/api/v1/config/harborforge.json', config, {
headers: { 'Content-Type': 'application/json' },
})
if (form.backend_base_url) {
localStorage.setItem('HF_BACKEND_BASE_URL', form.backend_base_url)
}
setStep(4)
} catch (err: any) {
setError(`Failed to save configuration: ${err.message}`)
} finally {
setSaving(false)
}
}
return (
<div className="setup-wizard">
<div className="setup-container">
<div className="setup-header">
<h1> HarborForge Setup Wizard</h1>
<div className="setup-steps">
{STEPS.map((s, i) => (
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
{i < step ? '✓' : i + 1}. {s}
</span>
))}
</div>
</div>
{error && <div className="setup-error">{error}</div>}
{/* Step 0: Welcome */}
{step === 0 && (
<div className="setup-step-content">
<h2>Welcome to HarborForge</h2>
<p>Agent/Human collaborative task management platform</p>
<div className="setup-info">
<p> The setup wizard connects to AbstractWizard via SSH tunnel. Ensure the port is forwarded:</p>
<code>ssh -L &lt;wizard_port&gt;:127.0.0.1:&lt;wizard_port&gt; user@your-server</code>
</div>
<button className="btn-primary" onClick={checkWizard}>
Connect to Wizard
</button>
{wizardOk === false && (
<p className="setup-hint">Connection failed. Check the SSH tunnel.</p>
)}
</div>
)}
{/* Step 1: Database */}
{step === 1 && (
<div className="setup-step-content">
<h2>Database configuration</h2>
<p className="text-dim">Configure MySQL connection (docker-compose defaults are fine if using the bundled MySQL).</p>
<div className="setup-form">
<label>Host <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
<label>Port <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
<label>Username <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
<label>Password <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
<label>Database <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(0)}>Back</button>
<button className="btn-primary" onClick={() => setStep(2)}>Next</button>
</div>
</div>
)}
{/* Step 2: Admin */}
{step === 2 && (
<div className="setup-step-content">
<h2>Admin account</h2>
<p className="text-dim">Create the first admin user</p>
<div className="setup-form">
<label>Username <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
<label>Password <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="Set admin password" /></label>
<label>Email <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
<label>Full name <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(1)}>Back</button>
<button className="btn-primary" onClick={() => {
if (!form.admin_password) { setError('Please set an admin password'); return }
setError('')
setStep(3)
}}>Next</button>
</div>
</div>
)}
{/* Step 3: Backend */}
{step === 3 && (
<div className="setup-step-content">
<h2>Backend URL</h2>
<p className="text-dim">Configure the HarborForge backend API URL</p>
<div className="setup-form">
<label>Backend Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://backend:8000" /></label>
</div>
<div className="setup-nav">
<button className="btn-back" onClick={() => setStep(2)}>Back</button>
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
{saving ? 'Saving...' : 'Finish setup'}
</button>
</div>
</div>
)}
{/* Step 4: Done */}
{step === 4 && (
<div className="setup-step-content">
<div className="setup-done">
<h2> Setup complete!</h2>
<p>Configuration saved to AbstractWizard.</p>
<div className="setup-info">
<p>Restart services on the server:</p>
<code>docker compose restart</code>
<p style={{ marginTop: '1rem' }}>After the backend starts, refresh this page to go to login.</p>
<p>Admin account: <strong>{form.admin_username}</strong></p>
</div>
<button className="btn-primary" onClick={onComplete}>
Refresh to check
</button>
</div>
</div>
)}
</div>
</div>
)
}