Распаковал архив Proxmox-VPS-Panel.rar и добавил содержимое в репозиторий
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { api } from "../api.js";
|
||||
|
||||
const NOVNC_CDN = "https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/lib/rfb.js";
|
||||
|
||||
export default function ConsoleViewer({ instanceId }) {
|
||||
const containerRef = useRef(null);
|
||||
const rfbRef = useRef(null);
|
||||
const [status, setStatus] = useState("Подключение…");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
const info = await api.getConsole(instanceId);
|
||||
const { default: RFB } = await import(/* @vite-ignore */ NOVNC_CDN);
|
||||
if (cancelled) return;
|
||||
|
||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const wsUrl =
|
||||
`${proto}://${window.location.host}/api/console/ws?` +
|
||||
`node=${encodeURIComponent(info.node)}&vmid=${info.vmid}&guest_type=${info.guest_type}` +
|
||||
`&port=${info.port}&ticket=${encodeURIComponent(info.ticket)}`;
|
||||
|
||||
const rfb = new RFB(containerRef.current, wsUrl);
|
||||
rfb.addEventListener("connect", () => !cancelled && setStatus("Подключено"));
|
||||
rfb.addEventListener("disconnect", () => !cancelled && setStatus("Соединение закрыто"));
|
||||
rfbRef.current = rfb;
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(err.message || String(err));
|
||||
}
|
||||
}
|
||||
|
||||
connect();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
rfbRef.current?.disconnect?.();
|
||||
};
|
||||
}, [instanceId]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="status-row" style={{ marginBottom: 8 }}>
|
||||
<span className={`dot ${error ? "error" : "running"}`} /> {error ? "Ошибка консоли" : status}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="error-box">
|
||||
{error}. Консоль VNC зависит от версии Proxmox и настроек аутентификации — см. README, раздел
|
||||
«Консоль VNC».
|
||||
</div>
|
||||
)}
|
||||
<div className="console-frame" ref={containerRef} style={{ width: "100%", height: 480 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api.js";
|
||||
|
||||
const STATUS_LABEL = {
|
||||
creating: "создаётся", running: "работает", stopped: "остановлен",
|
||||
error: "ошибка", deleting: "удаляется", deleted: "удалён",
|
||||
};
|
||||
|
||||
function formatUptime(sec) {
|
||||
if (!sec || sec <= 0) return "—";
|
||||
const d = Math.floor(sec / 86400);
|
||||
const h = Math.floor((sec % 86400) / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
if (d > 0) return `${d}д ${h}ч`;
|
||||
if (h > 0) return `${h}ч ${m}м`;
|
||||
return `${m}м`;
|
||||
}
|
||||
|
||||
function formatMem(mb) {
|
||||
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} ГБ`;
|
||||
return `${fmt1(mb)} МБ`;
|
||||
}
|
||||
|
||||
function fmt1(v) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n.toFixed(1) : v;
|
||||
}
|
||||
|
||||
export default function InstanceCard({ instance, onAction, onDelete }) {
|
||||
const [ip, setIp] = useState("...");
|
||||
const [live, setLive] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (instance.status === "running") {
|
||||
api.getInstanceIp(instance.id).then((r) => setIp(r.ip)).catch(() => setIp("—"));
|
||||
api.getInstanceLive(instance.id).then((r) => setLive(r)).catch(() => setLive(null));
|
||||
const interval = setInterval(() => {
|
||||
api.getInstanceLive(instance.id).then((r) => setLive(r)).catch(() => {});
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
setIp("—");
|
||||
setLive(null);
|
||||
}
|
||||
}, [instance.id, instance.status]);
|
||||
|
||||
const cpuBar = live && live.cpu > 0 ? (
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
<span>🔥 {live.cpu}%</span>
|
||||
<div style={{ width: 50, height: 8, background: "#333", borderRadius: 4, overflow: "hidden" }}>
|
||||
<div style={{ width: `${Math.min(live.cpu, 100)}%`, height: "100%",
|
||||
background: live.cpu > 80 ? "#f44336" : live.cpu > 50 ? "#ff9800" : "#4caf50",
|
||||
borderRadius: 4, transition: "width 0.5s" }} />
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const memBar = live && live.mem_total > 0 ? (
|
||||
<div style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
<span>🧠 {fmt1(formatMem(live.mem_used / (1024 * 1024)))} / {fmt1(formatMem(live.mem_total / (1024 * 1024)))}</span>
|
||||
<div style={{ width: 50, height: 8, background: "#333", borderRadius: 4, overflow: "hidden" }}>
|
||||
<div style={{ width: `${fmt1(Math.min((live.mem_used / live.mem_total) * 100, 100))}%`, height: "100%",
|
||||
background: (live.mem_used / live.mem_total) > 0.8 ? "#f44336" : (live.mem_used / live.mem_total) > 0.5 ? "#ff9800" : "#4caf50",
|
||||
borderRadius: 4, transition: "width 0.5s" }} />
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="instance-card">
|
||||
<div className="status-row">
|
||||
<span className={`dot ${instance.status}`} />
|
||||
{STATUS_LABEL[instance.status] || instance.status}
|
||||
<span className="badge">{instance.guest_type === "vm" ? "VM" : "LXC"}</span>
|
||||
{live && live.status === "running" && (
|
||||
<span style={{ marginLeft: 8, fontSize: 12, color: "#aaa" }}>
|
||||
⏱ {formatUptime(live.uptime)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="instance-name">
|
||||
<Link to={`/instances/${instance.id}`}>{instance.name}</Link>
|
||||
</div>
|
||||
|
||||
{/* Live-показатели */}
|
||||
{live && live.status === "running" && (
|
||||
<div style={{ margin: "8px 0", display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }}>
|
||||
{cpuBar}
|
||||
{memBar}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Статическая информация */}
|
||||
<div className="instance-meta">
|
||||
vmid {instance.vmid} · node {instance.node}
|
||||
{instance.template && (
|
||||
<> · 💻 {instance.template.cores} vCPU · 🧠 {instance.template.memory_mb} МБ · 💾 {instance.template.disk_gb} ГБ</>
|
||||
)}
|
||||
</div>
|
||||
<div className="instance-meta" style={{ marginTop: 6 }}>
|
||||
🌐 <strong>{ip}</strong>
|
||||
{instance.ciuser && <> · 👤 {instance.ciuser}</>}
|
||||
{instance.root_password && (
|
||||
<span style={{ color: "#69f0ae" }}> · 🔑 {instance.root_password}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="instance-actions">
|
||||
<button className="btn btn-sm" disabled={instance.status !== "stopped"} onClick={() => onAction(instance.id, "start")}>Старт</button>
|
||||
<button className="btn btn-sm" disabled={instance.status !== "running"} onClick={() => onAction(instance.id, "reboot")}>Reboot</button>
|
||||
<button className="btn btn-sm" disabled={instance.status !== "running"} onClick={() => onAction(instance.id, "stop")}>Стоп</button>
|
||||
<button className="btn btn-sm btn-danger" onClick={() => onDelete(instance.id)}>Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user