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

This commit is contained in:
2026-08-09 22:20:33 +03:00
parent c891f7e6f1
commit 0fe0139d45
+89 -9
View File
@@ -68,7 +68,7 @@ export default function Dashboard() {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [refresh]); }, [refresh]);
// Фильтрация шаблонов: сначала по типу (vm/lxc), потом по ОС. // Шаблоны строго по типу: при выбранном "vm" — только VM, при "lxc" — только LXC.
const typeTemplates = useMemo( const typeTemplates = useMemo(
() => templates.filter((t) => t.guest_type === guestType), () => templates.filter((t) => t.guest_type === guestType),
[templates, guestType] [templates, guestType]
@@ -78,12 +78,17 @@ export default function Dashboard() {
[typeTemplates, selectedOS] [typeTemplates, selectedOS]
); );
// Сбрасываем выбранный тариф при смене типа — иначе может быть выбран // При смене типа — сбросить выбор ОС и тарифа. Иначе может остаться шаблон
// шаблон другого типа. // другого типа или несуществующая ОС.
useEffect(() => { useEffect(() => {
setTemplateId(""); setTemplateId("");
setSelectedPreset(null); setSelectedPreset(null);
}, [guestType]); // Если текущая выбранная ОС недоступна для нового типа — сбросить на дефолт.
if (!OS_OPTIONS.some((os) => os.key === selectedOS && filterByOS(typeTemplates, os.key).length > 0)) {
const firstAvailable = OS_OPTIONS.find((os) => filterByOS(typeTemplates, os.key).length > 0);
setSelectedOS(firstAvailable?.key || "ubuntu");
}
}, [guestType, typeTemplates]);
// Авто-выбор первого подходящего шаблона при изменении фильтров. // Авто-выбор первого подходящего шаблона при изменении фильтров.
useEffect(() => { useEffect(() => {
@@ -122,7 +127,7 @@ export default function Dashboard() {
name, name,
template_id: Number(templateId), template_id: Number(templateId),
ciuser: isLxc ? "" : (ciuser || ""), ciuser: isLxc ? "" : (ciuser || ""),
cipassword: isLxc ? (cipassword || "") : (cipassword || ""), cipassword: cipassword || "",
}); });
setName(""); setName("");
setCipassword(""); setCipassword("");
@@ -155,6 +160,8 @@ export default function Dashboard() {
const submitLabel = isLxc const submitLabel = isLxc
? (busy ? "⏳ Создаём..." : "📦 Создать LXC") ? (busy ? "⏳ Создаём..." : "📦 Создать LXC")
: (busy ? "⏳ Создаём..." : "🚀 Развернуть VM"); : (busy ? "⏳ Создаём..." : "🚀 Развернуть VM");
const hasTypeTemplates = typeTemplates.length > 0;
const typeLabel = isLxc ? "LXC-контейнеров" : "VM (виртуальных машин)";
return ( return (
<div className="container"> <div className="container">
@@ -206,8 +213,6 @@ export default function Dashboard() {
</div> </div>
</div> </div>
<div className="form-section"><label className="form-label">Система</label><div className="os-grid">{OS_OPTIONS.map((os) => { const active = selectedOS === os.key; const available = filterByOS(typeTemplates, os.key).length > 0; return <button key={os.key} type="button" disabled={!available} onClick={() => { setSelectedOS(os.key); setSelectedPreset(null); }} className={`os-card${active ? " active" : ""}${!available ? " disabled" : ""}`}><os.logo /><div><div className="os-name">{os.label}</div><div className="os-desc">{os.desc}</div></div></button>; })}</div></div>
<div className="form-row"> <div className="form-row">
<div className="field" style={{ flex: 2 }}><label>Имя сервера</label><input value={name} onChange={(event) => setName(event.target.value)} placeholder="my-server" required /></div> <div className="field" style={{ flex: 2 }}><label>Имя сервера</label><input value={name} onChange={(event) => setName(event.target.value)} placeholder="my-server" required /></div>
{isLxc ? ( {isLxc ? (
@@ -218,9 +223,84 @@ export default function Dashboard() {
<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 type="password" value={cipassword} onChange={(event) => setCipassword(event.target.value)} placeholder="••••••" required={isLxc} title={isLxc ? "Задайте root-пароль для LXC" : "Пароль для cloud-init"} /></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> {/* Система и тариф — только если есть шаблоны этого типа. */}
{!hasTypeTemplates ? (
<div className="form-section">
<div className="empty-box">
<div className="empty-box-icon">📦</div>
<div className="empty-box-title">Нет шаблонов для {typeLabel}</div>
<div className="empty-box-sub">
Перейдите в раздел <strong>«Шаблоны»</strong> слева
нажмите <strong>«🔍 Найти шаблоны на Proxmox»</strong>
импортируйте нужный шаблон. После этого он появится здесь.
</div>
</div>
</div>
) : (
<>
<div className="form-section">
<label className="form-label">Система</label>
<div className="os-grid">{OS_OPTIONS.map((os) => {
const active = selectedOS === os.key;
const available = filterByOS(typeTemplates, os.key).length > 0;
return (
<button
key={os.key}
type="button"
disabled={!available}
onClick={() => { setSelectedOS(os.key); setSelectedPreset(null); }}
className={`os-card${active ? " active" : ""}${!available ? " disabled" : ""}`}
>
<os.logo />
<div>
<div className="os-name">{os.label}</div>
<div className="os-desc">{available ? os.desc : "нет шаблонов"}</div>
</div>
</button>
);
})}</div>
</div>
<button type="submit" disabled={busy || !templateId || !name || (isLxc && !cipassword)} className="btn btn-primary submit-btn">{submitLabel}</button> <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="">Нет шаблонов для «{OS_OPTIONS.find((o) => o.key === selectedOS)?.label}»</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 || (isLxc && !cipassword) || !hasTypeTemplates} 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">Нажмите «+ Новый VM» или «+ Новый LXC» чтобы создать</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>}