From c0b958c59b9180709e0d43e5996ec3bcb82e788a Mon Sep 17 00:00:00 2001 From: host Date: Sat, 25 Jul 2026 04:29:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Dashboard.jsx | 382 +++++++++++++++++++++++++------ 1 file changed, 318 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 98a9b8a..e8a9d34 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,12 +1,35 @@ 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 WHITE = "#fff", LIGHT = "#bbb", GREEN = "#69f0ae", DIM = "#777", BG = "#1e1e1e", BG2 = "#2a2a2a", BORDER = "#444"; +const UbuntuLogo = () => ( + + + + + + + + + + + +); -const UbuntuLogo = () => (); -const DebianLogo = () => (); -const CentOSLogo = () => (); +const DebianLogo = () => ( + + + + +); + +const CentOSLogo = () => ( + + + + +); const OS_OPTIONS = [ { key: "ubuntu", label: "Ubuntu", desc: "Универсальный", logo: UbuntuLogo }, @@ -14,18 +37,35 @@ const OS_OPTIONS = [ { 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 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); + 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; + 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 StatCard({ label, value, tone = "default", icon }) { + return ( +
+
{icon}
+
+
{label}
+
{value}
+
+
+ ); } export default function Dashboard() { @@ -44,70 +84,284 @@ export default function Dashboard() { const refresh = useCallback(async () => { const [inst, tpl] = await Promise.all([api.listInstances(), api.listTemplates()]); - setInstances(inst); setTemplates(tpl); + setInstances(inst); + setTemplates(tpl); }, []); - useEffect(() => { refresh().finally(() => setLoading(false)); const i = setInterval(refresh, 8000); return () => clearInterval(i); }, [refresh]); + + useEffect(() => { + refresh().finally(() => setLoading(false)); + const i = setInterval(refresh, 8000); + return () => clearInterval(i); + }, [refresh]); + const osT = filterByOS(templates, selectedOS); 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) { + 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)); + } }, [selectedOS, selectedPreset, templates]); async function onCreate(e) { - e.preventDefault(); setError(""); setBusy(true); - try { 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); } + e.preventDefault(); + setError(""); + setBusy(true); + try { + 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); + } } - 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 running = instances.filter(i => i.status==="running").length; - const total = instances.length; + 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(i => i.status === "running").length, + stopped: instances.filter(i => i.status === "stopped").length, + error: instances.filter(i => i.status === "error").length, + }; return ( -
-
-

Мои VPS

-

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

- +
+
+
+

Мои VPS

+

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

+
+
- {showForm && ( -
- {error &&
{error}
} - -
- {OS_OPTIONS.map(os => { const active=selectedOS===os.key; const has=filterByOS(templates,os.key).length>0; return ( - );})} -
-
-
- setName(e.target.value)} placeholder="my-server" required style={{ width:"100%", padding:"9px 12px", borderRadius:8, border:`1px solid ${BORDER}`, background:BG2, color:WHITE, fontSize:14, boxSizing:"border-box" }}/>
-
- setCiuser(e.target.value)} placeholder="ubuntu" style={{ width:"100%", padding:"9px 12px", borderRadius:8, border:`1px solid ${BORDER}`, background:BG2, color:WHITE, fontSize:14, boxSizing:"border-box" }}/>
-
- setCipassword(e.target.value)} placeholder="••••••" style={{ width:"100%", padding:"9px 12px", borderRadius:8, border:`1px solid ${BORDER}`, background:BG2, color:WHITE, fontSize:14, boxSizing:"border-box" }}/>
-
- -
- - {PRESETS.map(p=>{const match=matchPreset(osT,p); const avail=!!match; const active=selectedPreset===p.key; return( - );})} -
- -
)} + {/* Виджеты-метрики */} + {stats.total > 0 && ( +
+ } + /> + } + /> + } + /> + {stats.error > 0 && ( + } + /> + )} +
+ )} - {loading ?
Загрузка...
- : instances.length===0 ?
☁️
Нет активных VPS
Нажмите «+ Новый VPS» чтобы создать
- :
{instances.map(inst=>())}
} -
); + {showForm && ( +
+ {error &&
{error}
} + +
+ +
+ {OS_OPTIONS.map(os => { + const active = selectedOS === os.key; + const has = filterByOS(templates, os.key).length > 0; + return ( + + ); + })} +
+
+ +
+
+ + setName(e.target.value)} + placeholder="my-server" + required + /> +
+
+ + setCiuser(e.target.value)} + placeholder="ubuntu" + /> +
+
+ + setCipassword(e.target.value)} + placeholder="••••••" + /> +
+
+ +
+ +
+ +
+ {PRESETS.map(p => { + const match = matchPreset(osT, p); + const avail = !!match; + const active = selectedPreset === p.key; + return ( + + ); + })} +
+
+
+ + +
+ )} + + {loading ? ( +
Загрузка...
+ ) : instances.length === 0 ? ( +
+
☁️
+
Нет активных VPS
+
Нажмите «+ Новый VPS» чтобы создать
+
+ ) : ( +
+ {instances.map(inst => ( + + ))} +
+ )} +
+ ); } + +// Иконки для виджетов. +const IconServer = () => ( + + + + + + +); +const IconPlay = () => ( + + + +); +const IconStop = () => ( + + + +); +const IconAlert = () => ( + + + + + +); \ No newline at end of file