diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index e8a9d34..8cc21dd 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,65 +1,33 @@ import React, { useEffect, useState, useCallback } 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: "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(tpl, os) { - if (!os) return tpl; - return tpl.filter(t => t.name.toLowerCase().includes(os)); -} -function matchPreset(tpl, p) { - const exact = tpl.find(t => t.cores === p.cpu && t.memory_mb === p.ram && t.disk_gb === p.disk); - if (exact) return exact; - return tpl - .filter(t => t.cores >= p.cpu && t.memory_mb >= p.ram && t.disk_gb >= p.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 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", icon }) { +function StatCard({ label, value, tone = "default" }) { return (
-
{icon}
{label}
{value}
@@ -71,55 +39,68 @@ function StatCard({ label, value, tone = "default", icon }) { 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("ubuntu"); + const [ciuser, setCiuser] = useState("root"); 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 [inst, tpl] = await Promise.all([api.listInstances(), api.listTemplates()]); - setInstances(inst); - setTemplates(tpl); + const [instanceList, templateList] = await Promise.all([ + api.listInstances(), + api.listTemplates(), + ]); + setInstances(instanceList); + setTemplates(templateList); }, []); useEffect(() => { refresh().finally(() => setLoading(false)); - const i = setInterval(refresh, 8000); - return () => clearInterval(i); + const timer = setInterval(refresh, 8000); + return () => clearInterval(timer); }, [refresh]); - const osT = filterByOS(templates, selectedOS); + const typeTemplates = templates.filter((template) => template.guest_type === activeTab); + const visibleInstances = instances.filter((instance) => instance.guest_type === activeTab); + useEffect(() => { - if (selectedPreset) { - const p = PRESETS.find(x => x.key === selectedPreset); - const m = matchPreset(osT, p); - if (m) setTemplateId(String(m.id)); - } else if (osT.length > 0) { - setTemplateId(String(osT[0].id)); + 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) : ""); } - }, [selectedOS, selectedPreset, templates]); + }, [activeTab, templates, selectedPreset]); + + function switchTab(tab) { + setActiveTab(tab); + setSelectedPreset(null); + setError(""); + } + + async function onCreate(event) { + event.preventDefault(); + if (busy || !templateId || !name.trim()) return; - async function onCreate(e) { - e.preventDefault(); setError(""); setBusy(true); try { await api.createInstance({ - name, + name: name.trim(), template_id: Number(templateId), - ciuser: ciuser || "", + // Для архивного LXC backend создаёт новый пароль root. + ciuser: activeTab === "vm" ? ciuser : "root", cipassword: cipassword || "", }); setName(""); setCipassword(""); setShowForm(false); - setSelectedPreset(null); await refresh(); } catch (err) { setError(err.message); @@ -138,7 +119,7 @@ export default function Dashboard() { } async function onDelete(id) { - if (!confirm("Удалить VPS?")) return; + if (!confirm("Удалить инстанс?")) return; try { await api.deleteInstance(id); await refresh(); @@ -147,12 +128,11 @@ export default function Dashboard() { } } - // Метрики для виджетов. const stats = { total: instances.length, - running: instances.filter(i => i.status === "running").length, - stopped: instances.filter(i => i.status === "stopped").length, - error: instances.filter(i => i.status === "error").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 ( @@ -161,207 +141,96 @@ export default function Dashboard() {

Мои VPS

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

-
- {/* Виджеты-метрики */} {stats.total > 0 && (
- } - /> - } - /> - } - /> - {stats.error > 0 && ( - } - /> - )} + + + + {stats.error > 0 && }
)} +
+ + +
+ {showForm && (
{error &&
{error}
} -
- -
- {OS_OPTIONS.map(os => { - const active = selectedOS === os.key; - const has = filterByOS(templates, os.key).length > 0; + + +
+ + {activeTab === "vm" && ( +
+ {PRESETS.map((preset) => { + const available = Boolean(matchPreset(typeTemplates, preset)); return ( ); })}
-
+ )}
-
- - setName(e.target.value)} - placeholder="my-server" - required - /> -
-
- - setCiuser(e.target.value)} - placeholder="ubuntu" - /> -
-
- - setCipassword(e.target.value)} - placeholder="••••••" - /> -
+
setName(event.target.value)} placeholder={activeTab === "lxc" ? "debian" : "my-server"} required />
+
setCiuser(event.target.value)} disabled={activeTab === "lxc"} />
+
setCipassword(event.target.value)} placeholder="необязательно" />
- -
- -
- -
- {PRESETS.map(p => { - const match = matchPreset(osT, p); - const avail = !!match; - const active = selectedPreset === p.key; - return ( - - ); - })} -
-
-
- -
)} - {loading ? ( -
Загрузка...
- ) : instances.length === 0 ? ( + {loading ?
Загрузка...
: visibleInstances.length === 0 ? (
☁️
-
Нет активных VPS
-
Нажмите «+ Новый VPS» чтобы создать
+
Нет {activeTab === "lxc" ? "LXC-контейнеров" : "виртуальных машин"}
+
Нажмите «+ Новый VPS», чтобы создать ресурс
) : (
- {instances.map(inst => ( - - ))} + {visibleInstances.map((instance) => )}
)}
); } - -// Иконки для виджетов. -const IconServer = () => ( - - - - - - -); -const IconPlay = () => ( - - - -); -const IconStop = () => ( - - - -); -const IconAlert = () => ( - - - - - -); \ No newline at end of file