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 (
+
+ );
}
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 && (
-
)}
+ {/* Виджеты-метрики */}
+ {stats.total > 0 && (
+
+ }
+ />
+ }
+ />
+ }
+ />
+ {stats.error > 0 && (
+ }
+ />
+ )}
+
+ )}
- {loading ?
Загрузка...
- : instances.length===0 ?
☁️
Нет активных VPS
Нажмите «+ Новый VPS» чтобы создать
- :
{instances.map(inst=>())}
}
-
);
+ {showForm && (
+
+ )}
+
+ {loading ? (
+
Загрузка...
+ ) : instances.length === 0 ? (
+
+
☁️
+
Нет активных VPS
+
Нажмите «+ Новый VPS» чтобы создать
+
+ ) : (
+
+ {instances.map(inst => (
+
+ ))}
+
+ )}
+
+ );
}
+
+// Иконки для виджетов.
+const IconServer = () => (
+
+);
+const IconPlay = () => (
+
+);
+const IconStop = () => (
+
+);
+const IconAlert = () => (
+
+);
\ No newline at end of file