Обновление файла

This commit is contained in:
2026-08-09 22:14:10 +03:00
parent 55c3da65f5
commit 8c0b049e1a
+40 -16
View File
@@ -45,7 +45,7 @@ export default function Dashboard() {
const [instances, setInstances] = useState([]); const [instances, setInstances] = useState([]);
const [templates, setTemplates] = useState([]); const [templates, setTemplates] = useState([]);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [guestType, setGuestType] = useState("vm"); // ← НОВОЕ: "vm" | "lxc" const [guestType, setGuestType] = useState("vm"); // "vm" | "lxc"
const [name, setName] = useState(""); const [name, setName] = useState("");
const [ciuser, setCiuser] = useState("ubuntu"); const [ciuser, setCiuser] = useState("ubuntu");
const [cipassword, setCipassword] = useState(""); const [cipassword, setCipassword] = useState("");
@@ -98,24 +98,36 @@ export default function Dashboard() {
} }
}, [selectedOS, selectedPreset, osTemplates]); }, [selectedOS, selectedPreset, osTemplates]);
// Открыть форму для конкретного типа — VM или LXC.
const openForm = (type) => {
setGuestType(type);
setShowForm(true);
};
const closeForm = () => {
setShowForm(false);
setError("");
};
async function onCreate(event) { async function onCreate(event) {
event.preventDefault(); event.preventDefault();
if (busy) return; if (busy) return;
setError(""); setError("");
setBusy(true); setBusy(true);
try { try {
// Для LXC ciuser/cipassword не используются — пароль генерируется на бэкенде. const isLxc = guestType === "lxc";
// Передаём пустые строки, чтобы схема Pydantic не ругалась. // Для LXC пользователь — root (зашит в бэкенде), пароль — ввод пользователя.
// Для VM — стандартная cloud-init пара.
await api.createInstance({ await api.createInstance({
name, name,
template_id: Number(templateId), template_id: Number(templateId),
ciuser: guestType === "vm" ? (ciuser || "") : "", ciuser: isLxc ? "" : (ciuser || ""),
cipassword: guestType === "vm" ? (cipassword || "") : "", cipassword: isLxc ? (cipassword || "") : (cipassword || ""),
}); });
setName(""); setName("");
setCipassword(""); setCipassword("");
setShowForm(false);
setSelectedPreset(null); setSelectedPreset(null);
setShowForm(false);
await refresh(); await refresh();
} catch (err) { setError(err.message); } } catch (err) { setError(err.message); }
finally { setBusy(false); } finally { setBusy(false); }
@@ -146,14 +158,30 @@ export default function Dashboard() {
return ( return (
<div className="container"> <div className="container">
<div className="dashboard-header"><div><h1 className="page-title">Мои VPS</h1><p className="page-sub">{stats.total === 0 ? "Нет активных серверов" : `${stats.running} из ${stats.total} запущено`}</p></div><button className={showForm ? "btn" : "btn btn-primary"} onClick={() => setShowForm((value) => !value)}>{showForm ? "✕ Отмена" : "+ Новый VPS"}</button></div> <div className="dashboard-header">
<div>
<h1 className="page-title">Мои VPS</h1>
<p className="page-sub">
{stats.total === 0 ? "Нет активных серверов" : `${stats.running} из ${stats.total} запущено`}
</p>
</div>
{!showForm && (
<div className="dashboard-actions">
<button className="btn" onClick={() => openForm("vm")}>🖥 + Новый VM</button>
<button className="btn btn-primary" onClick={() => openForm("lxc")}>📦 + Новый LXC</button>
</div>
)}
{showForm && (
<button className="btn" onClick={closeForm}> Отмена</button>
)}
</div>
{stats.total > 0 && <div className="stats-grid"><StatCard label="Всего" value={stats.total} icon={<IconServer />} /><StatCard label="Запущено" value={stats.running} tone="success" icon={<IconPlay />} /><StatCard label="Остановлено" value={stats.stopped} tone="muted" icon={<IconStop />} />{stats.error > 0 && <StatCard label="Ошибки" value={stats.error} tone="danger" icon={<IconAlert />} />}</div>} {stats.total > 0 && <div className="stats-grid"><StatCard label="Всего" value={stats.total} icon={<IconServer />} /><StatCard label="Запущено" value={stats.running} tone="success" icon={<IconPlay />} /><StatCard label="Остановлено" value={stats.stopped} tone="muted" icon={<IconStop />} />{stats.error > 0 && <StatCard label="Ошибки" value={stats.error} tone="danger" icon={<IconAlert />} />}</div>}
{showForm && <form onSubmit={onCreate} className="card create-form"> {showForm && <form onSubmit={onCreate} className="card create-form">
{error && <div className="error-box">{error}</div>} {error && <div className="error-box">{error}</div>}
{/* Тип: VM (cloud-init) или LXC (root, пароль генерируется автоматически). */} {/* Переключатель типа внутри формы — на случай если передумал. */}
<div className="form-section"> <div className="form-section">
<label className="form-label">Тип сервера</label> <label className="form-label">Тип сервера</label>
<div className="type-tabs"> <div className="type-tabs">
@@ -173,7 +201,7 @@ export default function Dashboard() {
> >
<span className="type-tab-icon">📦</span> <span className="type-tab-icon">📦</span>
<span className="type-tab-label">LXC</span> <span className="type-tab-label">LXC</span>
<span className="type-tab-desc">root · пароль генерируется</span> <span className="type-tab-desc">root · пароль задаёте сами</span>
</button> </button>
</div> </div>
</div> </div>
@@ -187,19 +215,15 @@ export default function Dashboard() {
) : ( ) : (
<div className="field"><label>Пользователь</label><input value={ciuser} onChange={(event) => setCiuser(event.target.value)} placeholder="ubuntu" /></div> <div className="field"><label>Пользователь</label><input value={ciuser} onChange={(event) => setCiuser(event.target.value)} placeholder="ubuntu" /></div>
)} )}
{isLxc ? ( <div className="field"><label>Пароль</label><input type="password" value={cipassword} onChange={(event) => setCipassword(event.target.value)} placeholder="••••••" required={isLxc} title={isLxc ? "Задайте root-пароль для LXC" : "Пароль для cloud-init"} /></div>
<div className="field"><label>Пароль</label><input value="•• сгенерируется автоматически ••" disabled readOnly title="Пароль будет сгенерирован сервером и показан один раз после создания" /></div>
) : (
<div className="field"><label>Пароль</label><input type="password" value={cipassword} onChange={(event) => setCipassword(event.target.value)} placeholder="••••••" /></div>
)}
</div> </div>
<div className="form-section"><label className="form-label">Тариф</label><div className="preset-row"><select value={templateId} onChange={(event) => setTemplateId(event.target.value)} className="preset-select">{osTemplates.length === 0 ? <option value="">Нет шаблонов импортируйте в разделе «Шаблоны»</option> : osTemplates.map((template) => <option key={template.id} value={template.id}>{template.name} · {template.cores} vCPU · {template.memory_mb} MB · {template.disk_gb} GB</option>)}</select><div className="preset-buttons">{PRESETS.map((preset) => { const match = matchPreset(osTemplates, preset); const available = Boolean(match); return <button key={preset.key} type="button" disabled={!available} onClick={() => { setSelectedPreset(preset.key); if (match) setTemplateId(String(match.id)); }} className={`preset-btn${selectedPreset === preset.key ? " active" : ""}${!available ? " disabled" : ""}`}><div className="preset-label">{preset.label}</div><div className="preset-status">{available ? "✓" : "—"}</div></button>; })}</div></div></div> <div className="form-section"><label className="form-label">Тариф</label><div className="preset-row"><select value={templateId} onChange={(event) => setTemplateId(event.target.value)} className="preset-select">{osTemplates.length === 0 ? <option value="">Нет шаблонов импортируйте в разделе «Шаблоны»</option> : osTemplates.map((template) => <option key={template.id} value={template.id}>{template.name} · {template.cores} vCPU · {template.memory_mb} MB · {template.disk_gb} GB</option>)}</select><div className="preset-buttons">{PRESETS.map((preset) => { const match = matchPreset(osTemplates, preset); const available = Boolean(match); return <button key={preset.key} type="button" disabled={!available} onClick={() => { setSelectedPreset(preset.key); if (match) setTemplateId(String(match.id)); }} className={`preset-btn${selectedPreset === preset.key ? " active" : ""}${!available ? " disabled" : ""}`}><div className="preset-label">{preset.label}</div><div className="preset-status">{available ? "✓" : "—"}</div></button>; })}</div></div></div>
<button type="submit" disabled={busy || !templateId || !name} className="btn btn-primary submit-btn">{submitLabel}</button> <button type="submit" disabled={busy || !templateId || !name || (isLxc && !cipassword)} className="btn btn-primary submit-btn">{submitLabel}</button>
</form>} </form>}
{loading ? <div className="empty-state">Загрузка...</div> : instances.length === 0 ? <div className="empty-state"><div className="empty-icon"></div><div className="empty-title">Нет активных VPS</div><div className="empty-sub">Нажмите «+ Новый VPS» чтобы создать</div></div> : <div className="instance-grid">{instances.map((instance) => <InstanceCard key={instance.id} instance={instance} onAction={onAction} onDelete={onDelete} />)}</div>} {loading ? <div className="empty-state">Загрузка...</div> : instances.length === 0 ? <div className="empty-state"><div className="empty-icon"></div><div className="empty-title">Нет активных VPS</div><div className="empty-sub">Нажмите «+ Новый VM» или «+ Новый LXC» чтобы создать</div></div> : <div className="instance-grid">{instances.map((instance) => <InstanceCard key={instance.id} instance={instance} onAction={onAction} onDelete={onDelete} />)}</div>}
</div> </div>
); );
} }