EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
MeshCentralTerminal.jsx
Go to the documentation of this file.
1/**
2 * Custom MeshCentral Terminal Component
3 * Direct WebSocket connection - no iframe needed
4 * Adapted from MeshCentral's client-side terminal code
5 */
6
7import { useEffect, useRef, useState } from 'react';
8import { Terminal } from 'xterm';
9import { FitAddon } from 'xterm-addon-fit';
10import 'xterm/css/xterm.css';
11
12export default function MeshCentralTerminal({ agentId, sessionToken }) {
13 const terminalRef = useRef(null);
14 const wsRef = useRef(null);
15 const xtermRef = useRef(null);
16 const fitAddonRef = useRef(null);
17 const [status, setStatus] = useState('connecting');
18
19 useEffect(() => {
20 if (!agentId || !sessionToken) return;
21
22 // Initialize xterm.js terminal
23 const terminal = new Terminal({
24 cursorBlink: true,
25 fontSize: 14,
26 fontFamily: 'Menlo, Monaco, "Courier New", monospace',
27 theme: {
28 background: '#1e1e1e',
29 foreground: '#d4d4d4'
30 }
31 });
32
33 const fitAddon = new FitAddon();
34 terminal.loadAddon(fitAddon);
35
36 terminal.open(terminalRef.current);
37 fitAddon.fit();
38
39 xtermRef.current = terminal;
40 fitAddonRef.current = fitAddon;
41
42 // Connect to backend WebSocket proxy
43 const wsUrl = `${import.meta.env.VITE_WS_URL || 'wss://rmm-psa-backend-t9f7k.ondigitalocean.app'}/api/meshcentral/terminal?sessionToken=${sessionToken}&agentId=${agentId}`;
44
45 const ws = new WebSocket(wsUrl);
46 wsRef.current = ws;
47
48 ws.onopen = () => {
49 console.log('✅ Terminal WebSocket connected');
50 setStatus('connected');
51 terminal.writeln('Terminal connected. Waiting for shell...\r\n');
52 };
53
54 ws.onmessage = (event) => {
55 try {
56 const message = JSON.parse(event.data);
57
58 if (message.error) {
59 terminal.writeln(`\r\n\x1b[31mError: ${message.error}\x1b[0m\r\n`);
60 setStatus('error');
61 return;
62 }
63
64 // Handle terminal output
65 if (message.type === 'console' && message.data) {
66 // MeshCentral sends base64 encoded terminal data
67 const output = atob(message.data);
68 terminal.write(output);
69 }
70 } catch (err) {
71 console.error('Error parsing terminal message:', err);
72 }
73 };
74
75 ws.onerror = (error) => {
76 console.error('Terminal WebSocket error:', error);
77 setStatus('error');
78 terminal.writeln('\r\n\x1b[31mConnection error\x1b[0m\r\n');
79 };
80
81 ws.onclose = () => {
82 console.log('Terminal WebSocket closed');
83 setStatus('disconnected');
84 terminal.writeln('\r\n\x1b[33mConnection closed\x1b[0m\r\n');
85 };
86
87 // Send terminal input to MeshCentral
88 terminal.onData((data) => {
89 if (ws.readyState === WebSocket.OPEN) {
90 // Encode input as base64 (MeshCentral protocol)
91 const encoded = btoa(data);
92 ws.send(JSON.stringify({
93 type: 'console',
94 action: 'input',
95 data: encoded
96 }));
97 }
98 });
99
100 // Handle window resize
101 const handleResize = () => {
102 fitAddon.fit();
103 if (ws.readyState === WebSocket.OPEN) {
104 ws.send(JSON.stringify({
105 type: 'console',
106 action: 'resize',
107 cols: terminal.cols,
108 rows: terminal.rows
109 }));
110 }
111 };
112
113 window.addEventListener('resize', handleResize);
114
115 // Cleanup
116 return () => {
117 window.removeEventListener('resize', handleResize);
118 if (ws.readyState === WebSocket.OPEN) {
119 ws.close();
120 }
121 terminal.dispose();
122 };
123 }, [agentId, sessionToken]);
124
125 return (
126 <div className="meshcentral-terminal">
127 <div className="terminal-header">
128 <span className={`status-indicator status-${status}`}></span>
129 <span className="status-text">
130 {status === 'connecting' && 'Connecting...'}
131 {status === 'connected' && 'Connected'}
132 {status === 'error' && 'Connection Error'}
133 {status === 'disconnected' && 'Disconnected'}
134 </span>
135 </div>
136
137 <div
138 ref={terminalRef}
139 className="terminal-container"
140 style={{ height: '500px', width: '100%' }}
141 />
142
143 <style jsx>{`
144 .meshcentral-terminal {
145 border: 1px solid #ccc;
146 border-radius: 4px;
147 overflow: hidden;
148 }
149
150 .terminal-header {
151 background: #2d2d2d;
152 padding: 8px 12px;
153 display: flex;
154 align-items: center;
155 gap: 8px;
156 }
157
158 .status-indicator {
159 width: 8px;
160 height: 8px;
161 border-radius: 50%;
162 }
163
164 .status-connecting { background: #ffa500; }
165 .status-connected { background: #00ff00; }
166 .status-error { background: #ff0000; }
167 .status-disconnected { background: #888; }
168
169 .status-text {
170 color: #d4d4d4;
171 font-size: 12px;
172 }
173
174 .terminal-container {
175 padding: 8px;
176 }
177 `}</style>
178 </div>
179 );
180}