This commit is contained in:
@@ -1015,9 +1015,11 @@ function navigateToView(viewName, opts = {}) {
|
||||
// Expose for auto-refresh poller
|
||||
if (typeof window !== 'undefined') window.currentView = viewName;
|
||||
|
||||
// Drop live log stream when leaving container details
|
||||
if (leavingDetails && typeof stopDetailsLogs === 'function') {
|
||||
stopDetailsLogs();
|
||||
// Drop live streams / PTYs when leaving container details
|
||||
if (leavingDetails) {
|
||||
if (typeof stopDetailsLogs === 'function') stopDetailsLogs();
|
||||
if (typeof cleanupDetailsTerminal === 'function') cleanupDetailsTerminal();
|
||||
if (typeof destroyDetailsStatsCharts === 'function') destroyDetailsStatsCharts();
|
||||
}
|
||||
|
||||
if (viewName === 'dashboard') {
|
||||
@@ -4141,6 +4143,10 @@ let currentContainerDetails = null;
|
||||
* Force exactly one container-details tab visible.
|
||||
* Bootstrap Tab.show() fails to hide the previous pane when its trigger lost
|
||||
* `active` (e.g. after a global nav-link reset), so panes stack.
|
||||
*
|
||||
* Programmatic class toggles do NOT fire Bootstrap shown/hidden events — every
|
||||
* live tab (logs / terminal / stats / processes) must start/stop here as well
|
||||
* as via the BS event listeners.
|
||||
*/
|
||||
function activateContainerDetailsTab(tabButtonId = 'overview-tab') {
|
||||
const tabList = document.getElementById('container-details-tabs');
|
||||
@@ -4155,16 +4161,28 @@ function activateContainerDetailsTab(tabButtonId = 'overview-tab') {
|
||||
? tabContent.querySelector(targetSelector)
|
||||
: null;
|
||||
|
||||
const wasLogs =
|
||||
document.getElementById('logs-tab')?.classList.contains('active') ||
|
||||
document.getElementById('logs-pane')?.classList.contains('active');
|
||||
const willBeLogs = tabButtonId === 'logs-tab';
|
||||
const isActive = (tabId, paneId) =>
|
||||
document.getElementById(tabId)?.classList.contains('active') ||
|
||||
document.getElementById(paneId)?.classList.contains('active');
|
||||
|
||||
// Programmatic class toggles do not fire Bootstrap shown/hidden events —
|
||||
// stop log stream when leaving Logs so the next open always restarts cleanly.
|
||||
const wasLogs = isActive('logs-tab', 'logs-pane');
|
||||
const wasTerminal = isActive('terminal-tab', 'terminal-pane');
|
||||
const wasStats = isActive('stats-tab', 'stats-pane');
|
||||
const willBeLogs = tabButtonId === 'logs-tab';
|
||||
const willBeTerminal = tabButtonId === 'terminal-tab';
|
||||
const willBeStats = tabButtonId === 'stats-tab';
|
||||
const willBeProcesses = tabButtonId === 'processes-tab';
|
||||
|
||||
// Leave handlers (before pane class flip)
|
||||
if (wasLogs && !willBeLogs && typeof stopDetailsLogs === 'function') {
|
||||
stopDetailsLogs();
|
||||
}
|
||||
if (wasTerminal && !willBeTerminal && typeof cleanupDetailsTerminal === 'function') {
|
||||
cleanupDetailsTerminal();
|
||||
}
|
||||
if (wasStats && !willBeStats && typeof destroyDetailsStatsCharts === 'function') {
|
||||
destroyDetailsStatsCharts();
|
||||
}
|
||||
|
||||
tabList.querySelectorAll('.nav-link').forEach((link) => {
|
||||
const active = link === targetBtn;
|
||||
@@ -4179,12 +4197,28 @@ function activateContainerDetailsTab(tabButtonId = 'overview-tab') {
|
||||
pane.classList.toggle('active', active);
|
||||
});
|
||||
|
||||
if (willBeLogs && currentContainerDetails?.Id && typeof startDetailsLogs === 'function') {
|
||||
startDetailsLogs(currentContainerDetails.Id);
|
||||
// Enter handlers (after pane is visible so FitAddon / charts measure correctly)
|
||||
const cid = currentContainerDetails?.Id;
|
||||
if (willBeLogs && cid && typeof startDetailsLogs === 'function') {
|
||||
startDetailsLogs(cid);
|
||||
}
|
||||
if (willBeTerminal && cid && typeof initDetailsTerminal === 'function') {
|
||||
initDetailsTerminal(cid);
|
||||
}
|
||||
if (willBeStats && currentContainerDetails && typeof updateContainerDetailsStats === 'function') {
|
||||
updateContainerDetailsStats(currentContainerDetails);
|
||||
}
|
||||
if (willBeProcesses && cid && typeof loadContainerTop === 'function') {
|
||||
loadContainerTop(cid);
|
||||
}
|
||||
}
|
||||
|
||||
function showContainerDetails(container) {
|
||||
// Stop streams/PTY from a previous container before switching
|
||||
if (typeof stopDetailsLogs === 'function') stopDetailsLogs();
|
||||
if (typeof cleanupDetailsTerminal === 'function') cleanupDetailsTerminal();
|
||||
if (typeof destroyDetailsStatsCharts === 'function') destroyDetailsStatsCharts();
|
||||
|
||||
currentContainerDetails = container;
|
||||
if (typeof window !== 'undefined') window.currentContainerDetails = container;
|
||||
navigateToView('container-details');
|
||||
@@ -6483,6 +6517,350 @@ async function startDetailsLogs(containerId) {
|
||||
window.startDetailsLogs = startDetailsLogs;
|
||||
window.stopDetailsLogs = stopDetailsLogs;
|
||||
|
||||
// ─── Container-details Terminal tab (serialized start/stop) ───────────────
|
||||
// Alternating timeout bug: hidden.bs.tab fired killTerminal without awaiting,
|
||||
// then shown.bs.tab started a new PTY — the late kill destroyed the new session.
|
||||
// All start/stop ops run on a single promise chain so kill always finishes first.
|
||||
|
||||
/** @type {null | {
|
||||
* xterm: import('@xterm/xterm').Terminal,
|
||||
* fitAddon: object,
|
||||
* fitController: object,
|
||||
* inputCoalescer: object,
|
||||
* onDataDisposable: { dispose?: () => void },
|
||||
* containerId: string,
|
||||
* gen: number,
|
||||
* }} */
|
||||
let detailsTerminalSession = null;
|
||||
/** Bumped on every stop/start to abandon stale async work */
|
||||
let detailsTerminalGen = 0;
|
||||
/** Serializes terminal start/stop so kill never races start */
|
||||
let detailsTerminalChain = Promise.resolve();
|
||||
let detailsTerminalFontSize = 14;
|
||||
let detailsTerminalTheme = 'dark';
|
||||
|
||||
const DETAILS_TERMINAL_THEMES = {
|
||||
dark: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
selectionBackground: '#4d4d4d',
|
||||
},
|
||||
light: {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
cursor: '#000000',
|
||||
selectionBackground: '#b3d4fc',
|
||||
},
|
||||
'solarized-dark': {
|
||||
background: '#002b36',
|
||||
foreground: '#839496',
|
||||
cursor: '#93a1a1',
|
||||
selectionBackground: '#073642',
|
||||
},
|
||||
'solarized-light': {
|
||||
background: '#fdf6e3',
|
||||
foreground: '#657b83',
|
||||
cursor: '#586e75',
|
||||
selectionBackground: '#eee8d5',
|
||||
},
|
||||
monokai: {
|
||||
background: '#272822',
|
||||
foreground: '#f8f8f2',
|
||||
cursor: '#f8f8f0',
|
||||
selectionBackground: '#49483e',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {() => Promise<void> | void} fn
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function enqueueDetailsTerminalOp(fn) {
|
||||
const run = detailsTerminalChain.then(
|
||||
() => fn(),
|
||||
() => fn()
|
||||
);
|
||||
detailsTerminalChain = run.catch((err) => {
|
||||
console.warn('[terminal] op failed', err?.message || err);
|
||||
});
|
||||
return run;
|
||||
}
|
||||
|
||||
function disposeDetailsTerminalLocal(session) {
|
||||
if (!session) return;
|
||||
try {
|
||||
session.inputCoalescer?.flush?.();
|
||||
session.inputCoalescer?.destroy?.();
|
||||
session.onDataDisposable?.dispose?.();
|
||||
session.fitController?.disconnect?.();
|
||||
session.xterm?.dispose?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down local xterm + remote PTY. Safe when already idle.
|
||||
* Bumps gen so any concurrent start is abandoned after it returns.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function cleanupDetailsTerminalNow() {
|
||||
detailsTerminalGen += 1;
|
||||
const session = detailsTerminalSession;
|
||||
detailsTerminalSession = null;
|
||||
|
||||
disposeDetailsTerminalLocal(session);
|
||||
|
||||
const el = document.getElementById('container-terminal-xterm');
|
||||
if (el) el.innerHTML = '';
|
||||
|
||||
const cid = session?.containerId;
|
||||
if (cid && manager.active?.connected) {
|
||||
try {
|
||||
await manager.request(Methods.killTerminal, { containerId: cid });
|
||||
} catch {
|
||||
// already gone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public: enqueue terminal cleanup (tab leave / leave details view).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function cleanupDetailsTerminal() {
|
||||
return enqueueDetailsTerminalOp(() => cleanupDetailsTerminalNow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a fresh details-tab PTY for containerId. Always awaits any prior kill.
|
||||
* @param {string} containerId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function initDetailsTerminal(containerId) {
|
||||
if (!containerId) return Promise.resolve();
|
||||
return enqueueDetailsTerminalOp(() => initDetailsTerminalNow(containerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} containerId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initDetailsTerminalNow(containerId) {
|
||||
// Fully stop any existing session first (await kill — fixes re-entry race)
|
||||
if (detailsTerminalSession) {
|
||||
await cleanupDetailsTerminalNow();
|
||||
} else {
|
||||
detailsTerminalGen += 1;
|
||||
}
|
||||
|
||||
const myGen = detailsTerminalGen;
|
||||
|
||||
let TerminalCtor;
|
||||
let FitAddonCtor;
|
||||
try {
|
||||
TerminalCtor = getTerminalCtor();
|
||||
FitAddonCtor = getFitAddonCtor();
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Terminal libraries not loaded', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const terminalContainer = document.getElementById('container-terminal-xterm');
|
||||
const wrap = document.getElementById('container-terminal-content');
|
||||
if (!terminalContainer) return;
|
||||
|
||||
// Abandoned while waiting for constructors / DOM
|
||||
if (myGen !== detailsTerminalGen) return;
|
||||
|
||||
if (wrap) {
|
||||
wrap.style.minHeight = wrap.style.minHeight || '360px';
|
||||
wrap.style.height = wrap.style.height || '100%';
|
||||
}
|
||||
terminalContainer.style.width = '100%';
|
||||
terminalContainer.style.height = '100%';
|
||||
terminalContainer.style.minHeight = '320px';
|
||||
terminalContainer.innerHTML = '';
|
||||
|
||||
const theme =
|
||||
DETAILS_TERMINAL_THEMES[detailsTerminalTheme] || defaultXtermOptions().theme;
|
||||
|
||||
const xterm = new TerminalCtor(
|
||||
defaultXtermOptions({
|
||||
fontSize: detailsTerminalFontSize,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
|
||||
const fitAddon = new FitAddonCtor();
|
||||
xterm.loadAddon(fitAddon);
|
||||
xterm.open(terminalContainer);
|
||||
xterm.writeln('\x1b[90mConnecting to container shell…\x1b[0m');
|
||||
|
||||
const sendResize = (cols, rows) => {
|
||||
if (!manager.active?.connected || !cols || !rows) return;
|
||||
if (myGen !== detailsTerminalGen) return;
|
||||
manager.event(Methods.terminalResize, { containerId, cols, rows });
|
||||
};
|
||||
|
||||
const fitController = createFitController(fitAddon, xterm, sendResize);
|
||||
fitController.observe(terminalContainer);
|
||||
if (wrap) fitController.observe(wrap);
|
||||
|
||||
const inputCoalescer = createInputCoalescer(({ data, encoding }) => {
|
||||
if (!manager.active?.connected) return;
|
||||
if (myGen !== detailsTerminalGen) return;
|
||||
manager.event(Methods.terminalInput, {
|
||||
containerId,
|
||||
data,
|
||||
encoding: encoding || 'utf8',
|
||||
});
|
||||
});
|
||||
|
||||
const onDataDisposable = xterm.onData((data) => {
|
||||
if (!manager.active?.connected) return;
|
||||
if (myGen !== detailsTerminalGen) return;
|
||||
inputCoalescer.push(data);
|
||||
});
|
||||
|
||||
// Wait a frame so the tab pane is visible (FitAddon needs real dimensions)
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
|
||||
if (myGen !== detailsTerminalGen) {
|
||||
disposeDetailsTerminalLocal({
|
||||
xterm,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dims = safeFit(fitAddon, xterm) || { cols: xterm.cols, rows: xterm.rows };
|
||||
|
||||
if (!manager.active?.connected) {
|
||||
xterm.writeln('\r\n\x1b[31m[ERROR] Not connected\x1b[0m');
|
||||
disposeDetailsTerminalLocal({
|
||||
xterm,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await manager.request(Methods.startTerminal, {
|
||||
containerId,
|
||||
cols: dims.cols,
|
||||
rows: dims.rows,
|
||||
tty: true,
|
||||
});
|
||||
} catch (err) {
|
||||
if (myGen === detailsTerminalGen) {
|
||||
xterm.writeln(`\r\n\x1b[31m[ERROR] ${err?.message || err}\x1b[0m`);
|
||||
}
|
||||
// Leave xterm up with the error if still current; otherwise dispose
|
||||
if (myGen !== detailsTerminalGen) {
|
||||
disposeDetailsTerminalLocal({
|
||||
xterm,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
});
|
||||
try {
|
||||
await manager.request(Methods.killTerminal, { containerId });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Keep a minimal session so user sees the error (no remote PTY)
|
||||
detailsTerminalSession = {
|
||||
xterm,
|
||||
fitAddon,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
containerId,
|
||||
gen: myGen,
|
||||
failed: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// User left the tab while startTerminal was in flight — kill the PTY we just opened
|
||||
if (myGen !== detailsTerminalGen) {
|
||||
disposeDetailsTerminalLocal({
|
||||
xterm,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
});
|
||||
try {
|
||||
await manager.request(Methods.killTerminal, { containerId });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
detailsTerminalSession = {
|
||||
xterm,
|
||||
fitAddon,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
containerId,
|
||||
gen: myGen,
|
||||
};
|
||||
|
||||
sendResize(dims.cols, dims.rows);
|
||||
xterm.focus();
|
||||
updateDetailsTerminalFontSizeDisplay();
|
||||
}
|
||||
|
||||
function appendDetailsTerminalOutput(data, encoding = 'base64') {
|
||||
if (!detailsTerminalSession?.xterm || detailsTerminalSession.failed) return;
|
||||
const text = decodePayload(data, encoding);
|
||||
if (text) detailsTerminalSession.xterm.write(text);
|
||||
}
|
||||
|
||||
function updateDetailsTerminalFontSizeDisplay() {
|
||||
const display = document.getElementById('terminal-font-size-display');
|
||||
if (display) display.textContent = String(detailsTerminalFontSize);
|
||||
}
|
||||
|
||||
function applyDetailsTerminalTheme(theme) {
|
||||
detailsTerminalTheme = theme;
|
||||
if (detailsTerminalSession?.xterm) {
|
||||
detailsTerminalSession.xterm.options.theme =
|
||||
DETAILS_TERMINAL_THEMES[theme] || DETAILS_TERMINAL_THEMES.dark;
|
||||
}
|
||||
}
|
||||
|
||||
function applyDetailsTerminalFont(size) {
|
||||
detailsTerminalFontSize = size;
|
||||
if (detailsTerminalSession?.xterm) {
|
||||
detailsTerminalSession.xterm.options.fontSize = size;
|
||||
detailsTerminalSession.fitController?.fitNow?.();
|
||||
}
|
||||
updateDetailsTerminalFontSizeDisplay();
|
||||
}
|
||||
|
||||
window.initDetailsTerminal = initDetailsTerminal;
|
||||
window.cleanupDetailsTerminal = cleanupDetailsTerminal;
|
||||
window.handleDetailsTerminalOutput = (data, containerId, encoding) => {
|
||||
if (
|
||||
detailsTerminalSession &&
|
||||
detailsTerminalSession.containerId === containerId &&
|
||||
detailsTerminalSession.gen === detailsTerminalGen
|
||||
) {
|
||||
appendDetailsTerminalOutput(data, encoding);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-apply container list / dashboard after settings change (e.g. hide-label filters).
|
||||
*/
|
||||
@@ -6836,240 +7214,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Terminal state for details tab
|
||||
let detailsTerminalSession = null;
|
||||
let terminalFontSize = 14;
|
||||
let terminalTheme = 'dark';
|
||||
|
||||
// Terminal theme configurations
|
||||
const terminalThemes = {
|
||||
dark: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
selectionBackground: '#4d4d4d'
|
||||
},
|
||||
light: {
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
cursor: '#000000',
|
||||
selectionBackground: '#b3d4fc'
|
||||
},
|
||||
'solarized-dark': {
|
||||
background: '#002b36',
|
||||
foreground: '#839496',
|
||||
cursor: '#93a1a1',
|
||||
selectionBackground: '#073642'
|
||||
},
|
||||
'solarized-light': {
|
||||
background: '#fdf6e3',
|
||||
foreground: '#657b83',
|
||||
cursor: '#586e75',
|
||||
selectionBackground: '#eee8d5'
|
||||
},
|
||||
monokai: {
|
||||
background: '#272822',
|
||||
foreground: '#f8f8f2',
|
||||
cursor: '#f8f8f0',
|
||||
selectionBackground: '#49483e'
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize terminal for details tab
|
||||
function initDetailsTerminal(containerId) {
|
||||
let TerminalCtor;
|
||||
let FitAddonCtor;
|
||||
try {
|
||||
TerminalCtor = getTerminalCtor();
|
||||
FitAddonCtor = getFitAddonCtor();
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Terminal libraries not loaded', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const terminalContainer = document.getElementById('container-terminal-xterm');
|
||||
const wrap = document.getElementById('container-terminal-content');
|
||||
if (!terminalContainer) return;
|
||||
|
||||
if (detailsTerminalSession) {
|
||||
cleanupDetailsTerminal();
|
||||
}
|
||||
|
||||
// Ensure host has size for FitAddon
|
||||
if (wrap) {
|
||||
wrap.style.minHeight = wrap.style.minHeight || '360px';
|
||||
wrap.style.height = wrap.style.height || '100%';
|
||||
}
|
||||
terminalContainer.style.width = '100%';
|
||||
terminalContainer.style.height = '100%';
|
||||
terminalContainer.style.minHeight = '320px';
|
||||
|
||||
const theme =
|
||||
terminalThemes[terminalTheme] ||
|
||||
defaultXtermOptions().theme;
|
||||
|
||||
const xterm = new TerminalCtor(
|
||||
defaultXtermOptions({
|
||||
fontSize: terminalFontSize,
|
||||
theme,
|
||||
})
|
||||
);
|
||||
|
||||
const fitAddon = new FitAddonCtor();
|
||||
xterm.loadAddon(fitAddon);
|
||||
|
||||
terminalContainer.innerHTML = '';
|
||||
xterm.open(terminalContainer);
|
||||
|
||||
const sendResize = (cols, rows) => {
|
||||
if (!manager.active?.connected || !cols || !rows) return;
|
||||
manager.event(Methods.terminalResize, { containerId, cols, rows });
|
||||
};
|
||||
|
||||
const fitController = createFitController(fitAddon, xterm, sendResize);
|
||||
fitController.observe(terminalContainer);
|
||||
if (wrap) fitController.observe(wrap);
|
||||
|
||||
const inputCoalescer = createInputCoalescer(({ data, encoding }) => {
|
||||
if (!manager.active?.connected) return;
|
||||
manager.event(Methods.terminalInput, {
|
||||
containerId,
|
||||
data,
|
||||
encoding: encoding || 'utf8',
|
||||
});
|
||||
});
|
||||
|
||||
const onDataDisposable = xterm.onData((data) => {
|
||||
if (!manager.active?.connected) return;
|
||||
inputCoalescer.push(data);
|
||||
});
|
||||
|
||||
detailsTerminalSession = {
|
||||
xterm,
|
||||
fitAddon,
|
||||
fitController,
|
||||
inputCoalescer,
|
||||
onDataDisposable,
|
||||
containerId,
|
||||
};
|
||||
|
||||
// Fit when tab is visible, then start PTY with size.
|
||||
// Server probes bash → sh → ash → … until a working shell is found.
|
||||
requestAnimationFrame(() => {
|
||||
const dims = safeFit(fitAddon, xterm) || { cols: xterm.cols, rows: xterm.rows };
|
||||
if (manager.active?.connected) {
|
||||
manager
|
||||
.request(Methods.startTerminal, {
|
||||
containerId,
|
||||
cols: dims.cols,
|
||||
rows: dims.rows,
|
||||
tty: true,
|
||||
})
|
||||
.catch((err) => {
|
||||
xterm.writeln(`\r\n\x1b[31m[ERROR] ${err.message}\x1b[0m`);
|
||||
});
|
||||
sendResize(dims.cols, dims.rows);
|
||||
}
|
||||
xterm.focus();
|
||||
});
|
||||
|
||||
updateTerminalFontSizeDisplay();
|
||||
}
|
||||
|
||||
// Cleanup terminal for details tab
|
||||
function cleanupDetailsTerminal() {
|
||||
if (!detailsTerminalSession) return;
|
||||
const sid = detailsTerminalSession.containerId;
|
||||
try {
|
||||
detailsTerminalSession.inputCoalescer?.flush();
|
||||
detailsTerminalSession.inputCoalescer?.destroy();
|
||||
if (sid && manager.active?.connected) {
|
||||
manager.request(Methods.killTerminal, { containerId: sid }).catch(() => {});
|
||||
}
|
||||
detailsTerminalSession.onDataDisposable?.dispose();
|
||||
detailsTerminalSession.fitController?.disconnect();
|
||||
detailsTerminalSession.xterm?.dispose();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
detailsTerminalSession = null;
|
||||
const el = document.getElementById('container-terminal-xterm');
|
||||
if (el) el.innerHTML = '';
|
||||
}
|
||||
|
||||
// Append terminal output
|
||||
function appendDetailsTerminalOutput(data, encoding = 'base64') {
|
||||
if (!detailsTerminalSession?.xterm) return;
|
||||
const text = decodePayload(data, encoding);
|
||||
if (text) detailsTerminalSession.xterm.write(text);
|
||||
}
|
||||
|
||||
// Update font size display
|
||||
function updateTerminalFontSizeDisplay() {
|
||||
const display = document.getElementById('terminal-font-size-display');
|
||||
if (display) {
|
||||
display.textContent = terminalFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply terminal theme
|
||||
function applyTerminalTheme(theme) {
|
||||
terminalTheme = theme;
|
||||
if (detailsTerminalSession && detailsTerminalSession.xterm) {
|
||||
detailsTerminalSession.xterm.options.theme = terminalThemes[theme];
|
||||
}
|
||||
}
|
||||
|
||||
// Set up terminal tab
|
||||
// Terminal tab — lifecycle is module-level (serialized start/stop).
|
||||
// Bootstrap events fire on user clicks; activateContainerDetailsTab covers
|
||||
// programmatic switches that do not emit BS events.
|
||||
const terminalTab = document.getElementById('terminal-tab');
|
||||
if (terminalTab) {
|
||||
terminalTab.addEventListener('shown.bs.tab', () => {
|
||||
if (currentContainerDetails) {
|
||||
if (currentContainerDetails?.Id) {
|
||||
initDetailsTerminal(currentContainerDetails.Id);
|
||||
}
|
||||
});
|
||||
|
||||
terminalTab.addEventListener('hidden.bs.tab', () => {
|
||||
cleanupDetailsTerminal();
|
||||
});
|
||||
}
|
||||
|
||||
// Terminal controls
|
||||
|
||||
const terminalFontDecreaseBtn = document.getElementById('terminal-font-decrease');
|
||||
const terminalFontIncreaseBtn = document.getElementById('terminal-font-increase');
|
||||
const terminalFontResetBtn = document.getElementById('terminal-font-reset');
|
||||
const terminalCopyBtn = document.getElementById('terminal-copy-btn');
|
||||
const terminalClearBtn = document.getElementById('terminal-clear-btn');
|
||||
const terminalThemeSelect = document.getElementById('terminal-theme-select');
|
||||
|
||||
const applyDetailsFont = (size) => {
|
||||
terminalFontSize = size;
|
||||
if (detailsTerminalSession?.xterm) {
|
||||
detailsTerminalSession.xterm.options.fontSize = size;
|
||||
detailsTerminalSession.fitController?.fitNow();
|
||||
}
|
||||
updateTerminalFontSizeDisplay();
|
||||
};
|
||||
|
||||
if (terminalFontDecreaseBtn) {
|
||||
terminalFontDecreaseBtn.addEventListener('click', () => {
|
||||
if (terminalFontSize > 8) applyDetailsFont(terminalFontSize - 1);
|
||||
if (detailsTerminalFontSize > 8) {
|
||||
applyDetailsTerminalFont(detailsTerminalFontSize - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (terminalFontIncreaseBtn) {
|
||||
terminalFontIncreaseBtn.addEventListener('click', () => {
|
||||
if (terminalFontSize < 28) applyDetailsFont(terminalFontSize + 1);
|
||||
if (detailsTerminalFontSize < 28) {
|
||||
applyDetailsTerminalFont(detailsTerminalFontSize + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (terminalFontResetBtn) {
|
||||
terminalFontResetBtn.addEventListener('click', () => applyDetailsFont(14));
|
||||
terminalFontResetBtn.addEventListener('click', () => applyDetailsTerminalFont(14));
|
||||
}
|
||||
|
||||
if (terminalCopyBtn) {
|
||||
terminalCopyBtn.addEventListener('click', () => {
|
||||
if (detailsTerminalSession && detailsTerminalSession.xterm) {
|
||||
if (detailsTerminalSession?.xterm) {
|
||||
const selection = detailsTerminalSession.xterm.getSelection();
|
||||
if (selection) {
|
||||
copyToClipboard(selection, terminalCopyBtn);
|
||||
@@ -7079,7 +7265,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (terminalClearBtn) {
|
||||
terminalClearBtn.addEventListener('click', async () => {
|
||||
if (!detailsTerminalSession?.xterm) return;
|
||||
@@ -7093,20 +7278,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (ok) detailsTerminalSession.xterm.clear();
|
||||
});
|
||||
}
|
||||
|
||||
if (terminalThemeSelect) {
|
||||
terminalThemeSelect.value = terminalTheme;
|
||||
terminalThemeSelect.value = detailsTerminalTheme;
|
||||
terminalThemeSelect.addEventListener('change', (e) => {
|
||||
applyTerminalTheme(e.target.value);
|
||||
applyDetailsTerminalTheme(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle terminal output for details tab
|
||||
window.handleDetailsTerminalOutput = (data, containerId, encoding) => {
|
||||
if (detailsTerminalSession && detailsTerminalSession.containerId === containerId) {
|
||||
appendDetailsTerminalOutput(data, encoding);
|
||||
}
|
||||
};
|
||||
updateDetailsTerminalFontSizeDisplay();
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user