Fix terminal
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Terminal from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
// Access Terminal and FitAddon from global window object (loaded via script tags)
|
||||
const Terminal = window.Terminal || window.xterm?.Terminal;
|
||||
const FitAddon = window.FitAddon || window.xterm?.FitAddon;
|
||||
|
||||
// DOM Elements
|
||||
const dockerTerminalModal = document.getElementById('docker-terminal-modal');
|
||||
|
||||
+18
-1
@@ -24,8 +24,19 @@ function closeAllModals() {
|
||||
const modals = document.querySelectorAll('.modal.show');
|
||||
modals.forEach(modal => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
modalInstance.hide();
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
} else {
|
||||
// If no instance exists, create one and hide it
|
||||
const newInstance = new bootstrap.Modal(modal);
|
||||
newInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Also explicitly close the deploy modal if it exists
|
||||
if (templateDeployModal) {
|
||||
templateDeployModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Show status indicator
|
||||
@@ -703,7 +714,13 @@ deployForm.addEventListener('submit', async (e) => {
|
||||
showStatusIndicator('Deploying container...');
|
||||
const successResponse = await deployDockerContainer(formData);
|
||||
hideStatusIndicator();
|
||||
|
||||
// Close all modals including the deploy modal
|
||||
closeAllModals();
|
||||
if (templateDeployModal) {
|
||||
templateDeployModal.hide();
|
||||
}
|
||||
|
||||
showAlert('success', successResponse.message || 'Container deployed successfully!');
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to deploy container:', error.message);
|
||||
|
||||
+160
-153
@@ -1,5 +1,18 @@
|
||||
import Terminal from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
// terminal.js — Pure JavaScript (no TS-only features) – works everywhere
|
||||
|
||||
// Access Terminal and FitAddon from global window object (loaded via script tags)
|
||||
// xterm.js UMD bundle exposes Terminal and FitAddon on window
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window.Terminal) {
|
||||
throw new Error('xterm.js not loaded. Make sure the script tag is included before this module.');
|
||||
}
|
||||
if (!window.FitAddon) {
|
||||
throw new Error('xterm-addon-fit not loaded. Make sure the script tag is included before this module.');
|
||||
}
|
||||
}
|
||||
|
||||
const Terminal = window.Terminal;
|
||||
const FitAddon = window.FitAddon;
|
||||
|
||||
// DOM Elements
|
||||
const terminalModal = document.getElementById('terminal-modal');
|
||||
@@ -8,248 +21,242 @@ const terminalContainer = document.getElementById('terminal-container');
|
||||
const tray = document.getElementById('tray');
|
||||
const terminalHeader = document.querySelector('#terminal-modal .header');
|
||||
|
||||
// Terminal variables
|
||||
let terminalSessions = {}; // Track terminal sessions per containerId
|
||||
let activeContainerId = null; // Currently displayed containerId
|
||||
// State
|
||||
let terminalSessions = {}; // { [containerId: string]: session }
|
||||
let activeContainerId = null;
|
||||
|
||||
// Variables for resizing
|
||||
// Resizing
|
||||
let isResizing = false;
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
// Store event listener references for cleanup
|
||||
let mousemoveHandler = null;
|
||||
let mouseupHandler = null;
|
||||
|
||||
// Kill Terminal button functionality
|
||||
document.getElementById('kill-terminal-btn').onclick = () => {
|
||||
killActiveTerminal();
|
||||
};
|
||||
// Kill button
|
||||
const killBtn = document.getElementById('kill-terminal-btn');
|
||||
if (killBtn) killBtn.addEventListener('click', killActiveTerminal);
|
||||
|
||||
// Kill the active terminal session
|
||||
function killActiveTerminal() {
|
||||
const containerId = activeContainerId;
|
||||
// -------------------------------------------------------------------
|
||||
// RESIZING
|
||||
// -------------------------------------------------------------------
|
||||
if (terminalHeader) {
|
||||
terminalHeader.addEventListener('mousedown', (e) => {
|
||||
// Ignore if click started on the close button
|
||||
if (e.target.closest('#kill-terminal-btn')) return;
|
||||
|
||||
if (containerId && terminalSessions[containerId]) {
|
||||
console.log(`[INFO] Killing terminal session for container: ${containerId}`);
|
||||
|
||||
// Send kill command to server
|
||||
window.sendCommand('killTerminal', { containerId });
|
||||
|
||||
// Clean up terminal session
|
||||
cleanUpTerminal(containerId);
|
||||
|
||||
// Hide the terminal modal if this was the active session
|
||||
if (activeContainerId === containerId) {
|
||||
terminalModal.style.display = 'none';
|
||||
activeContainerId = null;
|
||||
}
|
||||
} else {
|
||||
console.error('[ERROR] No terminal session found to kill.');
|
||||
}
|
||||
isResizing = true;
|
||||
startY = e.clientY;
|
||||
startHeight = terminalModal.offsetHeight;
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
// Start resizing when the mouse is down on the header
|
||||
terminalHeader.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
startY = e.clientY; // Track the initial Y position
|
||||
startHeight = terminalModal.offsetHeight; // Track the initial height
|
||||
document.body.style.cursor = 'ns-resize'; // Change cursor to indicate resizing
|
||||
e.preventDefault(); // Prevent text selection
|
||||
});
|
||||
|
||||
// Resize the modal while dragging
|
||||
mousemoveHandler = (e) => {
|
||||
if (isResizing) {
|
||||
const deltaY = startY - e.clientY; // Calculate how much the mouse moved
|
||||
const newHeight = Math.min(
|
||||
Math.max(startHeight + deltaY, 150), // Minimum height: 150px
|
||||
window.innerHeight * 0.9 // Maximum height: 90% of viewport height
|
||||
);
|
||||
if (!isResizing) return;
|
||||
|
||||
terminalModal.style.height = `${newHeight}px`; // Set new height
|
||||
terminalContainer.style.height = `${newHeight - 40}px`; // Adjust terminal container height
|
||||
const deltaY = startY - e.clientY;
|
||||
const newHeight = Math.max(150, Math.min(startHeight + deltaY, window.innerHeight * 0.9));
|
||||
|
||||
const activeSession = terminalSessions[activeContainerId];
|
||||
if (activeSession) {
|
||||
setTimeout(() => activeSession.fitAddon.fit(), 10); // Adjust terminal content
|
||||
}
|
||||
terminalModal.style.height = `${newHeight}px`;
|
||||
terminalContainer.style.height = `${newHeight - 60}px`;
|
||||
|
||||
const session = terminalSessions[activeContainerId];
|
||||
if (session && session.fitAddon) {
|
||||
requestAnimationFrame(() => session.fitAddon.fit());
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousemove', mousemoveHandler);
|
||||
|
||||
// Stop resizing when the mouse is released
|
||||
mouseupHandler = () => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
document.body.style.cursor = 'default'; // Reset cursor
|
||||
document.body.style.cursor = 'default';
|
||||
document.body.style.userSelect = '';
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', mousemoveHandler);
|
||||
document.addEventListener('mouseup', mouseupHandler);
|
||||
|
||||
// Start terminal session
|
||||
// -------------------------------------------------------------------
|
||||
// START TERMINAL
|
||||
// -------------------------------------------------------------------
|
||||
function startTerminal(containerId, containerName) {
|
||||
if (!window.activePeer) {
|
||||
console.error('[ERROR] No active peer for terminal.');
|
||||
console.error('[ERROR] No active peer connection.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse if already exists
|
||||
if (terminalSessions[containerId]) {
|
||||
console.log(`[INFO] Terminal session already exists for container: ${containerId}`);
|
||||
if (activeContainerId !== containerId || terminalModal.style.display === 'none') {
|
||||
switchTerminal(containerId);
|
||||
} else {
|
||||
console.log(`[INFO] Terminal for container ${containerId} is already active`);
|
||||
}
|
||||
switchTerminal(containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[INFO] Creating new terminal session for container: ${containerId}`);
|
||||
|
||||
const xterm = new Terminal({
|
||||
cursorBlink: true,
|
||||
theme: { background: '#000000', foreground: '#ffffff' },
|
||||
cursorStyle: 'block',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
selectionBackground: '#4d4d4d',
|
||||
},
|
||||
scrollback: 10000,
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
xterm.loadAddon(fitAddon);
|
||||
|
||||
const terminalDiv = document.createElement('div');
|
||||
terminalDiv.style.width = '100%';
|
||||
terminalDiv.style.height = '100%';
|
||||
terminalDiv.style.display = 'none'; // Initially hidden
|
||||
terminalDiv.style.display = 'none';
|
||||
terminalContainer.appendChild(terminalDiv);
|
||||
|
||||
xterm.open(terminalDiv);
|
||||
fitAddon.fit();
|
||||
|
||||
const onDataDisposable = xterm.onData((data) => {
|
||||
console.log(`[DEBUG] Sending terminal input for container ${containerId}: ${data}`);
|
||||
window.activePeer.write(
|
||||
JSON.stringify({
|
||||
type: 'terminalInput',
|
||||
containerId,
|
||||
data: btoa(data),
|
||||
encoding: 'base64',
|
||||
})
|
||||
);
|
||||
const encoded = btoa(unescape(encodeURIComponent(data)));
|
||||
window.activePeer.write(JSON.stringify({
|
||||
type: 'terminalInput',
|
||||
containerId,
|
||||
data: encoded,
|
||||
encoding: 'base64',
|
||||
}));
|
||||
});
|
||||
|
||||
terminalSessions[containerId] = {
|
||||
xterm,
|
||||
fitAddon,
|
||||
onDataDisposable,
|
||||
output: '',
|
||||
container: terminalDiv,
|
||||
name: containerName,
|
||||
resizeListener: null,
|
||||
container: terminalDiv,
|
||||
};
|
||||
|
||||
console.log(`[INFO] Starting terminal for container: ${containerId}`);
|
||||
window.activePeer.write(
|
||||
JSON.stringify({ command: 'startTerminal', args: { containerId } })
|
||||
);
|
||||
window.activePeer.write(JSON.stringify({
|
||||
command: 'startTerminal',
|
||||
args: { containerId }
|
||||
}));
|
||||
|
||||
switchTerminal(containerId);
|
||||
}
|
||||
|
||||
// Switch to a terminal session
|
||||
// -------------------------------------------------------------------
|
||||
// SWITCH TERMINAL
|
||||
// -------------------------------------------------------------------
|
||||
function switchTerminal(containerId) {
|
||||
const session = terminalSessions[containerId];
|
||||
if (!session) {
|
||||
console.error(`[ERROR] No terminal session found for container: ${containerId}`);
|
||||
return;
|
||||
}
|
||||
if (!session) return;
|
||||
|
||||
// Hide previous
|
||||
if (activeContainerId && activeContainerId !== containerId) {
|
||||
const currentSession = terminalSessions[activeContainerId];
|
||||
if (currentSession) {
|
||||
if (currentSession.resizeListener) {
|
||||
window.removeEventListener('resize', currentSession.resizeListener);
|
||||
currentSession.resizeListener = null;
|
||||
const prev = terminalSessions[activeContainerId];
|
||||
if (prev) {
|
||||
prev.container.style.display = 'none';
|
||||
if (prev.resizeListener) {
|
||||
window.removeEventListener('resize', prev.resizeListener);
|
||||
}
|
||||
currentSession.container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show current
|
||||
session.container.style.display = 'block';
|
||||
setTimeout(() => session.fitAddon.fit(), 10);
|
||||
requestAnimationFrame(() => session.fitAddon.fit());
|
||||
|
||||
terminalTitle.textContent = `Terminal — ${session.name}`;
|
||||
terminalTitle.dataset.containerId = containerId;
|
||||
terminalTitle.textContent = `Container Terminal: ${session.name}`;
|
||||
terminalModal.style.display = 'flex';
|
||||
activeContainerId = containerId;
|
||||
|
||||
removeFromTray(containerId);
|
||||
|
||||
console.log(`[INFO] Switched to terminal for container: ${containerId}`);
|
||||
|
||||
// Window resize handler
|
||||
if (session.resizeListener) {
|
||||
window.removeEventListener('resize', session.resizeListener);
|
||||
}
|
||||
session.resizeListener = () => session.fitAddon.fit();
|
||||
window.addEventListener('resize', session.resizeListener);
|
||||
|
||||
removeFromTray(containerId);
|
||||
}
|
||||
|
||||
// Append terminal output
|
||||
function appendTerminalOutput(data, containerId, encoding) {
|
||||
// -------------------------------------------------------------------
|
||||
// APPEND OUTPUT
|
||||
// -------------------------------------------------------------------
|
||||
function appendTerminalOutput(data, containerId, encoding = 'base64') {
|
||||
const session = terminalSessions[containerId];
|
||||
if (!session) {
|
||||
console.error(`[ERROR] No terminal session found for container: ${containerId}`);
|
||||
return;
|
||||
}
|
||||
if (!session) return;
|
||||
|
||||
let outputData;
|
||||
let text = data;
|
||||
if (encoding === 'base64') {
|
||||
outputData = atob(data);
|
||||
} else {
|
||||
outputData = data;
|
||||
}
|
||||
|
||||
session.xterm.write(outputData);
|
||||
}
|
||||
|
||||
// Remove terminal from tray
|
||||
function removeFromTray(containerId) {
|
||||
const trayItem = document.querySelector(`.tray-item[data-id="${containerId}"]`);
|
||||
if (trayItem) {
|
||||
trayItem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up terminal session
|
||||
function cleanUpTerminal(containerId) {
|
||||
const session = terminalSessions[containerId];
|
||||
if (session) {
|
||||
session.xterm.dispose();
|
||||
session.onDataDisposable.dispose();
|
||||
if (session.resizeListener) {
|
||||
window.removeEventListener('resize', session.resizeListener);
|
||||
try {
|
||||
text = decodeURIComponent(escape(atob(data)));
|
||||
} catch (e) {
|
||||
console.error('Base64 decode failed', e);
|
||||
return;
|
||||
}
|
||||
session.container.parentNode.removeChild(session.container);
|
||||
delete terminalSessions[containerId];
|
||||
console.log(`[INFO] Cleaned up terminal for container: ${containerId}`);
|
||||
} else {
|
||||
console.error(`[ERROR] No terminal session to clean up for container: ${containerId}`);
|
||||
}
|
||||
|
||||
session.xterm.write(text);
|
||||
}
|
||||
|
||||
// Clean up all terminals
|
||||
function cleanUpAllTerminals() {
|
||||
Object.keys(terminalSessions).forEach(cleanUpTerminal);
|
||||
// -------------------------------------------------------------------
|
||||
// CLEANUP & UTILS
|
||||
// -------------------------------------------------------------------
|
||||
function removeFromTray(containerId) {
|
||||
const item = document.querySelector(`.tray-item[data-id="${containerId}"]`);
|
||||
if (item) item.remove();
|
||||
}
|
||||
|
||||
function killActiveTerminal() {
|
||||
if (!activeContainerId) return;
|
||||
|
||||
const containerId = activeContainerId;
|
||||
if (window.sendCommand) {
|
||||
window.sendCommand('killTerminal', { containerId });
|
||||
}
|
||||
cleanUpTerminal(containerId);
|
||||
|
||||
terminalModal.style.display = 'none';
|
||||
activeContainerId = null;
|
||||
|
||||
// Remove document-level event listeners
|
||||
if (mousemoveHandler) {
|
||||
document.removeEventListener('mousemove', mousemoveHandler);
|
||||
mousemoveHandler = null;
|
||||
}
|
||||
if (mouseupHandler) {
|
||||
document.removeEventListener('mouseup', mouseupHandler);
|
||||
mouseupHandler = null;
|
||||
}
|
||||
|
||||
console.log('[INFO] All terminal sessions cleaned up.');
|
||||
}
|
||||
|
||||
// Expose functions to app.js
|
||||
export { startTerminal, appendTerminalOutput, cleanUpAllTerminals };
|
||||
function cleanUpTerminal(containerId) {
|
||||
const session = terminalSessions[containerId];
|
||||
if (!session) return;
|
||||
|
||||
session.xterm.dispose();
|
||||
session.onDataDisposable.dispose();
|
||||
if (session.resizeListener) {
|
||||
window.removeEventListener('resize', session.resizeListener);
|
||||
}
|
||||
if (session.container && session.container.parentNode) {
|
||||
session.container.parentNode.removeChild(session.container);
|
||||
}
|
||||
|
||||
delete terminalSessions[containerId];
|
||||
}
|
||||
|
||||
function cleanUpAllTerminals() {
|
||||
Object.keys(terminalSessions).forEach(cleanUpTerminal);
|
||||
terminalSessions = {};
|
||||
activeContainerId = null;
|
||||
terminalModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// EXPORT
|
||||
// -------------------------------------------------------------------
|
||||
export {
|
||||
startTerminal,
|
||||
appendTerminalOutput,
|
||||
switchTerminal,
|
||||
killActiveTerminal,
|
||||
cleanUpTerminal,
|
||||
cleanUpAllTerminals,
|
||||
};
|
||||
Reference in New Issue
Block a user