import React, { useEffect, useState, useCallback } from "react"; import { api } from "../api.js"; import InstanceCard from "../components/InstanceCard.jsx"; 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 matchPreset(templates, preset) { return templates.find( (template) => template.cores === preset.cpu && template.memory_mb === preset.ram && template.disk_gb === preset.disk, ) || 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" }) { return (
{label}
{value}
); } export default function Dashboard() { const [instances, setInstances] = useState([]); const [templates, setTemplates] = useState([]); const [activeTab, setActiveTab] = useState("vm"); const [showForm, setShowForm] = useState(false); const [name, setName] = useState(""); const [ciuser, setCiuser] = useState("root"); const [cipassword, setCipassword] = useState(""); const [templateId, setTemplateId] = useState(""); 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 = templates.filter((template) => template.guest_type === activeTab); const visibleInstances = instances.filter((instance) => instance.guest_type === activeTab); useEffect(() => { if (selectedPreset && activeTab === "vm") { const preset = PRESETS.find((item) => item.key === selectedPreset); const match = preset ? matchPreset(typeTemplates, preset) : null; setTemplateId(match ? String(match.id) : ""); } else { setTemplateId(typeTemplates.length ? String(typeTemplates[0].id) : ""); } }, [activeTab, templates, selectedPreset]); function switchTab(tab) { setActiveTab(tab); setSelectedPreset(null); setError(""); } async function onCreate(event) { event.preventDefault(); if (busy || !templateId || !name.trim()) return; setError(""); setBusy(true); try { await api.createInstance({ name: name.trim(), template_id: Number(templateId), // Для архивного LXC backend создаёт новый пароль root. ciuser: activeTab === "vm" ? ciuser : "root", cipassword: cipassword || "", }); setName(""); setCipassword(""); 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("Удалить инстанс?")) 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, }; return (

Мои VPS

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

{stats.total > 0 && (
{stats.error > 0 && }
)}
{showForm && (
{error &&
{error}
}
{activeTab === "vm" && (
{PRESETS.map((preset) => { const available = Boolean(matchPreset(typeTemplates, preset)); return ( ); })}
)}
setName(event.target.value)} placeholder={activeTab === "lxc" ? "debian" : "my-server"} required />
setCiuser(event.target.value)} disabled={activeTab === "lxc"} />
setCipassword(event.target.value)} placeholder="необязательно" />
{activeTab === "lxc" &&

Для архивного шаблона Proxmox создаст новый root-пароль.

} {error &&
{error}
}
)} {loading ?
Загрузка...
: visibleInstances.length === 0 ? (
☁️
Нет {activeTab === "lxc" ? "LXC-контейнеров" : "виртуальных машин"}
Нажмите «+ Новый VPS», чтобы создать ресурс
) : (
{visibleInstances.map((instance) => )}
)}
); }