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.
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
(() => {
|
||||
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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user