Files
ome-cockpit/ui/ssh.js
T
mo b73ff9465c Initial import of Dell OpenManage Cockpit for ATC.
Interactive OME fleet UI with topology, KPI popups, in-browser SSH, chat, tickets, and full inventory landscape.
2026-07-17 04:00:38 +02:00

288 lines
7.8 KiB
JavaScript

(() => {
const $ = (sel) => document.querySelector(sel);
let term = null;
let fitAddon = null;
let socket = null;
let connected = false;
let connectInFlight = false;
let currentHost = "";
function protoWs() {
return location.protocol === "https:" ? "wss" : "ws";
}
function rememberUser(user) {
if (user) localStorage.setItem("cockpit_ssh_user", user);
}
function rememberedUser() {
return localStorage.getItem("cockpit_ssh_user") || "";
}
function openSshModal(opts = {}) {
const modal = $("#ssh-modal");
const scrim = $("#scrim");
if (!modal) return;
currentHost = opts.host || "";
$("#ssh-host").value = currentHost;
$("#ssh-port").value = opts.port || 22;
$("#ssh-user").value = opts.username || rememberedUser();
$("#ssh-pass").value = "";
$("#ssh-device").textContent = opts.name || currentHost || "SSH session";
$("#ssh-sub").textContent = opts.model
? `${opts.model}${opts.ip ? " · " + opts.ip : ""}`
: opts.ip || currentHost || "—";
$("#ssh-status").textContent = "Enter username and password, then Connect.";
$("#ssh-status").className = "ssh-status";
$("#ssh-connect-btn").disabled = false;
$("#ssh-term-wrap").classList.add("hidden");
$("#ssh-login").classList.remove("hidden");
modal.classList.remove("hidden");
modal.setAttribute("aria-hidden", "false");
scrim?.classList.add("open");
if (scrim) scrim.dataset.mode = "ssh";
setTimeout(() => {
const u = $("#ssh-user");
if (u && !u.value) u.focus();
else $("#ssh-pass")?.focus();
}, 50);
}
function closeSshModal() {
disconnectSsh();
const modal = $("#ssh-modal");
modal?.classList.add("hidden");
modal?.setAttribute("aria-hidden", "true");
const scrim = $("#scrim");
if (scrim?.dataset.mode === "ssh") {
scrim.classList.remove("open");
delete scrim.dataset.mode;
}
}
function setStatus(msg, kind) {
const el = $("#ssh-status");
if (!el) return;
el.textContent = msg;
el.className = "ssh-status" + (kind ? " " + kind : "");
}
function ensureTerminal() {
if (term) return term;
if (!window.Terminal) {
setStatus("Terminal library missing — hard refresh (Ctrl+Shift+R) or check /vendor/xterm/", "error");
$("#ssh-login")?.classList.remove("hidden");
return null;
}
term = new window.Terminal({
cursorBlink: true,
fontFamily: "IBM Plex Mono, ui-monospace, Menlo, monospace",
fontSize: 13,
lineHeight: 1.2,
theme: {
background: "#071018",
foreground: "#e8f4ff",
cursor: "#3dffe0",
selectionBackground: "rgba(0,168,232,0.35)",
},
allowProposedApi: true,
});
const FitCtor = window.FitAddon?.FitAddon || window.FitAddon;
fitAddon = FitCtor ? new FitCtor() : null;
if (fitAddon) term.loadAddon(fitAddon);
term.open($("#ssh-term"));
term.onData((data) => {
if (!socket || socket.readyState !== WebSocket.OPEN || !connected) return;
socket.send(data);
});
window.addEventListener("resize", () => {
try {
fitAddon?.fit();
sendResize();
} catch (_) {}
});
return term;
}
function sendResize() {
if (!socket || socket.readyState !== WebSocket.OPEN || !term) return;
socket.send(
JSON.stringify({
type: "resize",
cols: term.cols,
rows: term.rows,
})
);
}
function disconnectSsh() {
connected = false;
try {
socket?.close();
} catch (_) {}
socket = null;
try {
term?.reset();
} catch (_) {}
}
async function connectSsh() {
if (connectInFlight) return;
const host = ($("#ssh-host")?.value || "").trim();
const username = ($("#ssh-user")?.value || "").trim();
const password = $("#ssh-pass")?.value || "";
const port = Number($("#ssh-port")?.value || 22);
if (!host || !username) {
setStatus("Host and username are required", "error");
return;
}
if (!password) {
setStatus("Password is required", "error");
$("#ssh-pass")?.focus();
return;
}
rememberUser(username);
connectInFlight = true;
disconnectSsh();
$("#ssh-login").classList.add("hidden");
$("#ssh-term-wrap").classList.remove("hidden");
const t = ensureTerminal();
if (!t) return;
t.reset();
t.focus();
try {
fitAddon?.fit();
} catch (_) {}
setStatus(`Connecting as ${username}@${host}:${port}…`, "info");
$("#ssh-connect-btn").disabled = true;
const ws = new WebSocket(`${protoWs()}://${location.host}/ws/ssh`);
socket = ws;
ws.onopen = () => {
ws.send(
JSON.stringify({
type: "auth",
host,
port,
username,
password,
cols: t.cols || 120,
rows: t.rows || 36,
term: "xterm-256color",
})
);
// clear password from DOM after send
if ($("#ssh-pass")) $("#ssh-pass").value = "";
};
ws.onmessage = (ev) => {
const data = String(ev.data || "");
if (data.startsWith("{") && data.includes('"type"')) {
try {
const msg = JSON.parse(data);
if (msg.type === "ready") {
connected = true;
connectInFlight = false;
setStatus(msg.message || "Connected", "ok");
$("#ssh-connect-btn").disabled = false;
$("#ssh-connect-btn").textContent = "Reconnect";
try {
fitAddon?.fit();
sendResize();
t.focus();
} catch (_) {}
return;
}
if (msg.type === "status") {
setStatus(msg.message || "", "info");
return;
}
if (msg.type === "error") {
connected = false;
connectInFlight = false;
setStatus(msg.message || "SSH error", "error");
$("#ssh-connect-btn").disabled = false;
$("#ssh-login").classList.remove("hidden");
return;
}
if (msg.type === "pong") return;
} catch (_) {
// fall through as terminal output
}
}
t.write(data);
};
ws.onerror = () => {
setStatus("WebSocket error", "error");
$("#ssh-connect-btn").disabled = false;
$("#ssh-login").classList.remove("hidden");
};
ws.onclose = () => {
connected = false;
connectInFlight = false;
setStatus("Disconnected", "info");
$("#ssh-connect-btn").disabled = false;
$("#ssh-connect-btn").textContent = "Connect";
};
}
function openSshForNode(node) {
if (!node?.ip) {
alert("No management IP for this device");
return;
}
openSshModal({
host: node.ip,
ip: node.ip,
name: node.name,
model: node.model,
username: rememberedUser(),
});
}
function bind() {
$("#btn-ssh-close")?.addEventListener("click", closeSshModal);
$("#btn-ssh-disconnect")?.addEventListener("click", () => {
disconnectSsh();
$("#ssh-login").classList.remove("hidden");
setStatus("Disconnected. Enter credentials to reconnect.", "info");
});
$("#ssh-form")?.addEventListener("submit", (e) => {
e.preventDefault();
connectSsh();
});
$("#ssh-connect-btn")?.addEventListener("click", (e) => {
e.preventDefault();
connectSsh();
});
// Scrim close for SSH
$("#scrim")?.addEventListener("click", () => {
if ($("#scrim")?.dataset.mode === "ssh") closeSshModal();
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !$("#ssh-modal")?.classList.contains("hidden")) {
closeSshModal();
}
});
}
bind();
window.cockpitSsh = {
open: openSshForNode,
openModal: openSshModal,
close: closeSshModal,
};
})();