import React, { useEffect, useState, useCallback, useMemo } from "react"; import { Link } from "react-router-dom"; import { api } from "../api.js"; import InstanceCard from "../components/InstanceCard.jsx"; const UbuntuLogo = () => ( ); const DebianLogo = () => ( ); const CentOSLogo = () => ( ); const OS_OPTIONS = [ { key: "ubuntu", label: "Ubuntu", desc: "Универсальный", logo: UbuntuLogo }, { key: "debian", label: "Debian", desc: "Стабильный", logo: DebianLogo }, { key: "centos", label: "CentOS", desc: "Корпоративный", logo: CentOSLogo }, ]; const PRESETS = [ { key: "small", label: "S", cpu: 1, ram: 1024, disk: 10 }, { key: "medium", label: "M", cpu: 2, ram: 2048, disk: 20 }, { key: "large", label: "L", cpu: 4, ram: 8192, disk: 50 }, { key: "xlarge", label: "XL", cpu: 8, ram: 16384, disk: 100 }, ]; function filterByOS(templates, os) { if (!os) return templates; return templates.filter((template) => template.name.toLowerCase().includes(os)); } function matchPreset(templates, preset) { const exact = templates.find((template) => template.cores === preset.cpu && template.memory_mb === preset.ram && template.disk_gb === preset.disk); if (exact) return exact; return templates.filter((template) => template.cores >= preset.cpu && template.memory_mb >= preset.ram && template.disk_gb >= preset.disk) .sort((a, b) => (a.cores + a.memory_mb / 1024 + a.disk_gb) - (b.cores + b.memory_mb / 1024 + b.disk_gb))[0] || null; } function StatCard({ label, value, tone = "default", icon }) { return
{icon}
{label}
{value}
; } export default function Dashboard() { const [instances, setInstances] = useState([]); const [templates, setTemplates] = useState([]); const [showForm, setShowForm] = useState(false); const [guestType, setGuestType] = useState("vm"); // "vm" | "lxc" — задаётся кнопкой в шапке, внутри формы не меняется const [name, setName] = useState(""); const [ciuser, setCiuser] = useState("ubuntu"); const [cipassword, setCipassword] = useState(""); const [templateId, setTemplateId] = useState(""); const [selectedOS, setSelectedOS] = useState("ubuntu"); const [selectedPreset, setSelectedPreset] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [loading, setLoading] = useState(true); const refresh = useCallback(async () => { const [instanceList, templateList] = await Promise.all([api.listInstances(), api.listTemplates()]); setInstances(instanceList); setTemplates(templateList); }, []); useEffect(() => { refresh().finally(() => setLoading(false)); const timer = setInterval(refresh, 8000); return () => clearInterval(timer); }, [refresh]); // Шаблоны строго по выбранному типу. const typeTemplates = useMemo( () => templates.filter((t) => t.guest_type === guestType), [templates, guestType] ); const osTemplates = useMemo( () => filterByOS(typeTemplates, selectedOS), [typeTemplates, selectedOS] ); // При смене типа — сбросить выбор тарифа и подобрать доступную ОС. useEffect(() => { setTemplateId(""); setSelectedPreset(null); 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(() => { if (selectedPreset) { const preset = PRESETS.find((item) => item.key === selectedPreset); const match = preset ? matchPreset(osTemplates, preset) : null; if (match) setTemplateId(String(match.id)); } else if (osTemplates.length) { setTemplateId(String(osTemplates[0].id)); } else { setTemplateId(""); } }, [selectedOS, selectedPreset, osTemplates]); // Открыть форму для конкретного типа — VM или LXC. const openForm = (type) => { setGuestType(type); setName(""); setCipassword(""); setSelectedPreset(null); setShowForm(true); }; const closeForm = () => { setShowForm(false); setError(""); }; async function onCreate(event) { event.preventDefault(); if (busy) return; setError(""); setBusy(true); try { const isLxc = guestType === "lxc"; await api.createInstance({ name, template_id: Number(templateId), ciuser: isLxc ? "" : (ciuser || ""), cipassword: cipassword || "", }); setShowForm(false); await refresh(); } catch (err) { setError(err.message); } finally { setBusy(false); } } async function onAction(id, action) { try { await api.instanceAction(id, action); await refresh(); } catch (err) { setError(err.message); } } async function onDelete(id) { if (!confirm("Удалить VPS?")) return; try { await api.deleteInstance(id); await refresh(); } catch (err) { setError(err.message); } } const stats = { total: instances.length, running: instances.filter((item) => item.status === "running").length, stopped: instances.filter((item) => item.status === "stopped").length, error: instances.filter((item) => item.status === "error").length, }; const isLxc = guestType === "lxc"; const hasTypeTemplates = typeTemplates.length > 0; return (

Мои VPS

{stats.total === 0 ? "Нет активных серверов" : `${stats.running} из ${stats.total} запущено`}

{!showForm && (
)} {showForm && ( )}
{stats.total > 0 &&
} />} />} />{stats.error > 0 && } />}
} {showForm && (
{error &&
{error}
} {/* Статический заголовок типа — переключатель убран, тип задаётся только кнопкой в шапке при открытии формы. */}
{isLxc ? ( <> 📦 Создаём LXC-контейнер · пользователь root · пароль задаёте сами ) : ( <> 🖥 Создаём виртуальную машину · cloud-init · любой пользователь )}
setName(event.target.value)} placeholder="my-server" required />
{isLxc ? (
) : (
setCiuser(event.target.value)} placeholder="ubuntu" />
)}
setCipassword(event.target.value)} placeholder="••••••" required={isLxc} title={isLxc ? "Задайте root-пароль для LXC" : "Пароль для cloud-init"} />
{/* Система и тариф — только если есть шаблоны этого типа. */} {!hasTypeTemplates ? (
📦
Нет шаблонов для {isLxc ? "LXC-контейнеров" : "VM (виртуальных машин)"}
Перейдите в раздел «Шаблоны» слева → нажмите «🔍 Найти шаблоны на Proxmox» → импортируйте нужный шаблон. После этого он появится здесь.
) : ( <>
{OS_OPTIONS.map((os) => { const active = selectedOS === os.key; const available = filterByOS(typeTemplates, os.key).length > 0; return ( ); })}
{PRESETS.map((preset) => { const match = matchPreset(osTemplates, preset); const available = Boolean(match); return ( ); })}
)}
)} {loading ?
Загрузка...
: instances.length === 0 ?
☁️
Нет активных VPS
Нажмите «+ Новый VM» или «+ Новый LXC» чтобы создать
:
{instances.map((instance) => )}
}
); } const IconServer = () => ; const IconPlay = () => ; const IconStop = () => ; const IconAlert = () => ;