EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabTerminal.jsx
Go to the documentation of this file.
1import React, { useState, useEffect, useRef } from 'react';
2import { Terminal } from 'xterm';
3import { FitAddon } from 'xterm-addon-fit';
4import 'xterm/css/xterm.css';
5import { apiFetch } from '../../lib/api';
6
7export default function TabTerminal({ agentId, agentUuid }) {
8 const terminalRef = useRef(null);
9 const containerRef = useRef(null);
10 const wsRef = useRef(null);
11 const xtermRef = useRef(null);
12 const fitAddonRef = useRef(null);
13
14 const [status, setStatus] = useState('initializing');
15 const [error, setError] = useState(null);
16 const [nodeId, setNodeId] = useState(null);
17
18 // Fetch MeshCentral node ID
19 useEffect(() => {
20 async function fetchNodeId() {
21 try {
22 const response = await apiFetch(`/agent/${agentUuid || agentId}/meshcentral-node`);
23 if (!response.ok) {
24 throw new Error('Failed to fetch MeshCentral node ID');
25 }
26 const data = await response.json();
27 setNodeId(data.nodeId);
28 } catch (err) {
29 console.error('[Terminal] Error fetching node ID:', err);
30 setError(err.message);
31 setStatus('error');
32 }
33 }
34
35 if (agentUuid || agentId) {
36 fetchNodeId();
37 }
38 }, [agentId, agentUuid]);
39
40 // Initialize terminal and WebSocket connection
41 useEffect(() => {
42 if (!nodeId || !containerRef.current) return;
43
44 // Create terminal
45 const terminal = new Terminal({
46 cursorBlink: true,
47 fontSize: 14,
48 fontFamily: 'Menlo, Monaco, "Courier New", monospace',
49 theme: {
50 background: '#1e1e1e',
51 foreground: '#d4d4d4',
52 cursor: '#ffffff',
53 selection: '#264f78'
54 }
55 });
56
57 const fitAddon = new FitAddon();
58 terminal.loadAddon(fitAddon);
59 terminal.open(containerRef.current);
60 fitAddon.fit();
61
62 xtermRef.current = terminal;
63 fitAddonRef.current = fitAddon;
64
65 // Get session token
66 const token = localStorage.getItem('token');
67
68 // Connect to MeshCentral terminal WebSocket
69 const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
70 const ws = new WebSocket(
71 `${protocol}://${window.location.host}/api/meshcentral/terminal?nodeId=${nodeId}&token=${token}`
72 );
73
74 ws.onopen = () => {
75 console.log('[Terminal] WebSocket connected');
76 setStatus('connected');
77 terminal.write('\r\n*** Connected to MeshCentral Terminal ***\r\n\r\n');
78 };
79
80 ws.onmessage = (event) => {
81 const data = JSON.parse(event.data);
82
83 if (data.type === 'console') {
84 // Decode base64 output and write to terminal
85 const output = atob(data.data);
86 terminal.write(output);
87 } else if (data.type === 'error') {
88 terminal.write(`\r\n[Error] ${data.message}\r\n`);
89 setError(data.message);
90 }
91 };
92
93 ws.onerror = (err) => {
94 console.error('[Terminal] WebSocket error:', err);
95 setStatus('error');
96 setError('WebSocket connection error');
97 terminal.write('\r\n*** Connection error ***\r\n');
98 };
99
100 ws.onclose = () => {
101 console.log('[Terminal] WebSocket disconnected');
102 setStatus('disconnected');
103 terminal.write('\r\n*** Disconnected from terminal ***\r\n');
104 };
105
106 // Handle terminal input
107 terminal.onData((data) => {
108 if (ws.readyState === WebSocket.OPEN) {
109 // Encode input as base64 and send to backend
110 const encoded = btoa(data);
111 ws.send(JSON.stringify({
112 action: 'input',
113 data: encoded
114 }));
115 }
116 });
117
118 // Handle window resize
119 const handleResize = () => {
120 fitAddon.fit();
121 if (ws.readyState === WebSocket.OPEN) {
122 ws.send(JSON.stringify({
123 action: 'resize',
124 cols: terminal.cols,
125 rows: terminal.rows
126 }));
127 }
128 };
129
130 window.addEventListener('resize', handleResize);
131 wsRef.current = ws;
132
133 // Cleanup
134 return () => {
135 window.removeEventListener('resize', handleResize);
136 if (ws.readyState === WebSocket.OPEN) {
137 ws.close();
138 }
139 terminal.dispose();
140 };
141 }, [nodeId]);
142
143 if (error) {
144 return (
145 <div className="tab-container">
146 <h2>Terminal</h2>
147 <div className="card" style={{ textAlign: 'center', padding: '2rem' }}>
148 <div style={{ fontSize: '3rem', marginBottom: '1rem' }}>⚠️</div>
149 <h3>Connection Error</h3>
150 <p style={{ color: '#666' }}>{error}</p>
151 <p style={{ color: '#999', fontSize: '0.9rem', marginTop: '1rem' }}>
152 Make sure MeshCentral agent is installed and connected
153 </p>
154 </div>
155 </div>
156 );
157 }
158
159 if (status === 'initializing' && !nodeId) {
160 return (
161 <div className="tab-container">
162 <h2>Terminal</h2>
163 <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
164 <div className="spinner" style={{ margin: '0 auto 1rem' }}></div>
165 <p>Loading terminal...</p>
166 </div>
167 </div>
168 );
169 }
170
171 return (
172 <div className="tab-container">
173 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
174 <h2 style={{ margin: 0 }}>Terminal</h2>
175 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
176 <div style={{
177 width: '8px',
178 height: '8px',
179 borderRadius: '50%',
180 backgroundColor: status === 'connected' ? '#4caf50' : status === 'error' ? '#f44336' : '#ff9800'
181 }} />
182 <span style={{ fontSize: '0.9rem', color: '#666', textTransform: 'capitalize' }}>
183 {status}
184 </span>
185 </div>
186 </div>
187
188 <div className="card" style={{ padding: '0', overflow: 'hidden' }}>
189 <div
190 ref={containerRef}
191 style={{
192 width: '100%',
193 height: '600px',
194 padding: '8px'
195 }}
196 />
197 </div>
198 </div>
199 );
200}