feat: setup wizard page for first-deploy initialization
- SetupWizardPage: step-by-step config (DB, admin, project) - Connects directly to AbstractWizard via SSH tunnel (127.0.0.1) - App.tsx: detect backend health, show wizard if not ready - Auto-switch wizard to readonly after setup - Add VITE_WIZARD_PORT build arg - Add vite-env.d.ts for type safety
This commit is contained in:
222
src/pages/SetupWizardPage.tsx
Normal file
222
src/pages/SetupWizardPage.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
interface Props {
|
||||
wizardPort: number
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
interface SetupForm {
|
||||
// Admin
|
||||
admin_username: string
|
||||
admin_password: string
|
||||
admin_email: string
|
||||
admin_full_name: string
|
||||
// Database
|
||||
db_host: string
|
||||
db_port: number
|
||||
db_user: string
|
||||
db_password: string
|
||||
db_database: string
|
||||
// Backend
|
||||
backend_url: string
|
||||
// Default project
|
||||
project_name: string
|
||||
project_description: string
|
||||
}
|
||||
|
||||
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
|
||||
|
||||
export default function SetupWizardPage({ wizardPort, 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_url: '',
|
||||
project_name: 'Default',
|
||||
project_description: '默认项目',
|
||||
})
|
||||
|
||||
const wizardApi = axios.create({
|
||||
baseURL: `http://127.0.0.1:${wizardPort}`,
|
||||
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(`无法连接 AbstractWizard (127.0.0.1:${wizardPort})。请确认已通过 SSH 隧道映射端口:\nssh -L ${wizardPort}:127.0.0.1:${wizardPort} user@server`)
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
setError('')
|
||||
setSaving(true)
|
||||
try {
|
||||
const config = {
|
||||
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_url || undefined,
|
||||
default_project: form.project_name
|
||||
? { name: form.project_name, description: form.project_description }
|
||||
: undefined,
|
||||
}
|
||||
|
||||
await wizardApi.put('/api/v1/config/harborforge.json', config, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
// Switch wizard to readonly after setup
|
||||
await wizardApi.put('/api/v1/mode', { mode: 'readonly' }).catch(() => {})
|
||||
|
||||
setStep(4)
|
||||
} catch (err: any) {
|
||||
setError(`保存配置失败: ${err.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="setup-wizard">
|
||||
<div className="setup-container">
|
||||
<div className="setup-header">
|
||||
<h1>⚓ HarborForge 初始化向导</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>欢迎使用 HarborForge</h2>
|
||||
<p>Agent/人类协同任务管理平台</p>
|
||||
<div className="setup-info">
|
||||
<p>⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:</p>
|
||||
<code>ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server</code>
|
||||
</div>
|
||||
<button className="btn-primary" onClick={checkWizard}>
|
||||
连接 Wizard 开始配置
|
||||
</button>
|
||||
{wizardOk === false && (
|
||||
<p className="setup-hint">连接失败,请检查 SSH 隧道是否正确建立。</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Database */}
|
||||
{step === 1 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>数据库配置</h2>
|
||||
<p className="text-dim">配置 MySQL 连接信息(使用 docker-compose 内置 MySQL 可保持默认值)</p>
|
||||
<div className="setup-form">
|
||||
<label>主机 <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
|
||||
<label>端口 <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
|
||||
<label>用户名 <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
|
||||
<label>密码 <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
|
||||
<label>数据库名 <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)}>上一步</button>
|
||||
<button className="btn-primary" onClick={() => setStep(2)}>下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Admin */}
|
||||
{step === 2 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>管理员账号</h2>
|
||||
<p className="text-dim">创建首个管理员用户</p>
|
||||
<div className="setup-form">
|
||||
<label>用户名 <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
|
||||
<label>密码 <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="设置管理员密码" /></label>
|
||||
<label>邮箱 <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
|
||||
<label>姓名 <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)}>上一步</button>
|
||||
<button className="btn-primary" onClick={() => {
|
||||
if (!form.admin_password) { setError('请设置管理员密码'); return }
|
||||
setError('')
|
||||
setStep(3)
|
||||
}}>下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Project */}
|
||||
{step === 3 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>默认项目(可选)</h2>
|
||||
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
||||
<div className="setup-form">
|
||||
<label>项目名称 <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="留空则跳过" /></label>
|
||||
<label>项目描述 <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
|
||||
</div>
|
||||
<div className="setup-nav">
|
||||
<button className="btn-back" onClick={() => setStep(2)}>上一步</button>
|
||||
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
|
||||
{saving ? '保存中...' : '完成配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Done */}
|
||||
{step === 4 && (
|
||||
<div className="setup-step-content">
|
||||
<div className="setup-done">
|
||||
<h2>✅ 初始化完成!</h2>
|
||||
<p>配置已保存到 AbstractWizard,Wizard 已切换为只读模式。</p>
|
||||
<p>后端将在检测到配置后自动启动。</p>
|
||||
<div className="setup-info">
|
||||
<p>请等待后端启动完成,然后刷新页面进入登录界面。</p>
|
||||
<p>管理员账号: <strong>{form.admin_username}</strong></p>
|
||||
</div>
|
||||
<button className="btn-primary" onClick={onComplete}>
|
||||
刷新检查
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user