frontend/src/components/Terminal.vue
<script>
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
export default {
name: 'Terminal',
props: {
selectedPod: {
type: Object,
required: true
},
selectedNamespace: {
type: Object,
required: true
},
selectedCluster: {
type: Object,
required: true
}
},
data() {
return {
terminal: null,
fitAddon: null,
socket: null,
isConnected: false,
reconnectAttempts: 0,
maxReconnectAttempts: 3
}
},
mounted() {
this.initializeTerminal();
window.addEventListener('resize', this.onResize);
},
beforeUnmount() {
this.cleanup();
window.removeEventListener('resize', this.onResize);
},
methods: {
initializeTerminal() {
// Initialize xterm.js terminal
this.terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: '#1e1e1e',
foreground: '#ffffff',
cursor: '#ffffff',
selection: 'rgba(255, 255, 255, 0.3)',
black: '#000000',
blue: '#2472c8',
brightBlue: '#3b8eea',
brightCyan: '#29b8db',
brightGreen: '#23d18b',
brightMagenta: '#d670d6',
brightRed: '#f14c4c',
brightWhite: '#e5e5e5',
brightYellow: '#f5f543',
cyan: '#11a8cd',
green: '#0dbc79',
magenta: '#bc3fbc',
red: '#cd3131',
white: '#e5e5e5',
yellow: '#e5e510'
}
});
// Add the fit addon
this.fitAddon = new FitAddon();
this.terminal.loadAddon(this.fitAddon);
// Add web links addon
this.terminal.loadAddon(new WebLinksAddon());
// Open terminal in the container
this.terminal.open(this.$refs.terminal);
// Initial fit
setTimeout(() => {
this.fitAddon.fit();
}, 100);
// Connect to WebSocket
this.connectWebSocket();
},
connectWebSocket() {
// If already connecting, don't try again
if (this.isReconnecting) {
return;
}
this.isReconnecting = true;
// Close existing socket if any
if (this.socket) {
this.socket.close();
this.socket = null;
}
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.hostname}:8081/api/pods/exec?` +
`cluster=${encodeURIComponent(this.selectedCluster.server)}&` +
`namespace=${encodeURIComponent(this.selectedNamespace.name)}&` +
`pod=${encodeURIComponent(this.selectedPod.metadata.name)}`;
try {
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
this.isReconnecting = false;
this.terminal.writeln('Connected to pod shell...');
this.terminal.focus();
};
this.socket.onmessage = (event) => {
if (this.terminal) {
this.terminal.write(event.data);
}
};
this.socket.onclose = () => {
this.isConnected = false;
this.isReconnecting = false;
if (this.terminal) {
this.terminal.writeln('\r\nConnection closed');
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
this.isReconnecting = false;
if (this.terminal) {
this.terminal.writeln('\r\nError: Failed to connect to pod shell');
}
};
// Handle terminal input
if (!this.terminal._initialized) {
this.terminal.onData(data => {
if (this.isConnected && this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(data);
}
});
this.terminal._initialized = true;
}
} catch (error) {
console.error('Failed to connect to WebSocket:', error);
this.isReconnecting = false;
if (this.terminal) {
this.terminal.writeln('Failed to connect to pod shell');
}
}
},
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.terminal.writeln(`\r\nAttempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connectWebSocket();
}, 2000);
}
},
onResize() {
if (this.fitAddon) {
this.fitAddon.fit();
}
},
cleanup() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
if (this.terminal) {
if (this.fitAddon) {
try {
this.fitAddon.dispose();
} catch (e) {
console.log('FitAddon cleanup:', e);
}
this.fitAddon = null;
}
try {
this.terminal.dispose();
} catch (e) {
console.log('Terminal cleanup:', e);
}
this.terminal = null;
}
this.isConnected = false;
this.isReconnecting = false;
this.reconnectAttempts = 0;
},
closeTerminal() {
this.cleanup();
this.$emit('close');
}
}
}
</script>
<template>
<div class="terminal-container">
<div class="terminal-header">
<span>Terminal: {{ selectedPod?.metadata?.name }}</span>
<button @click="closeTerminal" class="close-btn">×</button>
</div>
<div id="terminal" ref="terminal" class="terminal-content"></div>
</div>
</template>
<style scoped>
.terminal-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 800px;
height: 500px;
background: #1e1e1e;
border: 1px solid #333;
border-radius: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 1000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #333;
color: white;
font-size: 14px;
user-select: none;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.close-btn:hover {
color: #ff5252;
}
.terminal-content {
flex: 1;
padding: 4px;
background-color: #1e1e1e;
text-align: left; /* Add this */
}
:deep(.xterm) {
padding: 8px;
height: 100%;
text-align: left; /* Add this */
}
:deep(.xterm-viewport) {
background-color: #1e1e1e !important;
}
/* Add these new styles */
:deep(.xterm-screen) {
text-align: left !important;
}
:deep(.xterm-rows) {
text-align: left !important;
}
</style>