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
;
}
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 && (
)}
{loading ?
Загрузка...
: instances.length === 0 ?
☁️
Нет активных VPS
Нажмите «+ Новый VM» или «+ Новый LXC» чтобы создать
:
{instances.map((instance) => )}
}
);
}
const IconServer = () => ;
const IconPlay = () => ;
const IconStop = () => ;
const IconAlert = () => ;