From 525fc52021304857ca8103e0ace58f8c98bcde3b Mon Sep 17 00:00:00 2001 From: host Date: Sun, 9 Aug 2026 19:25:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=B0=D1=82=20=D0=B2=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=BE=D0=BA=20VM=20=D0=B8=20LXC=20=D0=B2=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5=20=D0=9C=D0=BE=D0=B8?= =?UTF-8?q?=20VPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Dashboard.jsx | 225 +++++++++---------------------- 1 file changed, 63 insertions(+), 162 deletions(-) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 8cc21dd..dadbbb2 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,7 +1,23 @@ 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 }, @@ -9,52 +25,38 @@ const PRESETS = [ { 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 filterByOS(templates, os) { + if (!os) return templates; + return templates.filter((template) => template.name.toLowerCase().includes(os)); } -function StatCard({ label, value, tone = "default" }) { - return ( -
-
-
{label}
-
{value}
-
-
- ); +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 [activeTab, setActiveTab] = useState("vm"); const [showForm, setShowForm] = useState(false); const [name, setName] = useState(""); - const [ciuser, setCiuser] = useState("root"); + 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(), - ]); + const [instanceList, templateList] = await Promise.all([api.listInstances(), api.listTemplates()]); setInstances(instanceList); setTemplates(templateList); }, []); @@ -65,67 +67,42 @@ export default function Dashboard() { return () => clearInterval(timer); }, [refresh]); - const typeTemplates = templates.filter((template) => template.guest_type === activeTab); - const visibleInstances = instances.filter((instance) => instance.guest_type === activeTab); - + const osTemplates = filterByOS(templates, selectedOS); useEffect(() => { - if (selectedPreset && activeTab === "vm") { + if (selectedPreset) { 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) : ""); + const match = preset ? matchPreset(osTemplates, preset) : null; + if (match) setTemplateId(String(match.id)); + } else if (osTemplates.length) { + setTemplateId(String(osTemplates[0].id)); } - }, [activeTab, templates, selectedPreset]); - - function switchTab(tab) { - setActiveTab(tab); - setSelectedPreset(null); - setError(""); - } + }, [selectedOS, selectedPreset, templates]); async function onCreate(event) { event.preventDefault(); - if (busy || !templateId || !name.trim()) return; - + if (busy) 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 || "", - }); + await api.createInstance({ name, template_id: Number(templateId), ciuser: ciuser || "", cipassword: cipassword || "" }); setName(""); setCipassword(""); setShowForm(false); + setSelectedPreset(null); await refresh(); - } catch (err) { - setError(err.message); - } finally { - setBusy(false); - } + } 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); - } + 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); - } + if (!confirm("Удалить VPS?")) return; + try { await api.deleteInstance(id); await refresh(); } + catch (err) { setError(err.message); } } const stats = { @@ -137,100 +114,24 @@ export default function Dashboard() { return (
-
-
-

Мои VPS

-

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

-
- -
+

Мои VPS

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

- {stats.total > 0 && ( -
- - - - {stats.error > 0 && } -
- )} + {stats.total > 0 &&
} />} />} />{stats.error > 0 && } />}
} -
- - -
+ {showForm &&
+ {error &&
{error}
} +
{OS_OPTIONS.map((os) => { const active = selectedOS === os.key; const available = filterByOS(templates, os.key).length > 0; return ; })}
+
setName(event.target.value)} placeholder="my-server" required />
setCiuser(event.target.value)} placeholder="ubuntu" />
setCipassword(event.target.value)} placeholder="••••••" />
+
{PRESETS.map((preset) => { const match = matchPreset(osTemplates, preset); const available = Boolean(match); return ; })}
+ +
} - {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) => )} -
- )} + {loading ?
Загрузка...
: instances.length === 0 ?
☁️
Нет активных VPS
Нажмите «+ Новый VPS» чтобы создать
:
{instances.map((instance) => )}
}
); } + +const IconServer = () => ; +const IconPlay = () => ; +const IconStop = () => ; +const IconAlert = () => ;