EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
RDSViewer.jsx
Go to the documentation of this file.
1/**
2 * RDS (Remote Desktop) Viewer Component
3 *
4 * Integrates with existing WebSocket RDS infrastructure to display remote desktop
5 * sessions using HTML canvas rendering with mouse/keyboard input.
6 *
7 * Features:
8 * - Real-time screen streaming via WebSocket
9 * - Mouse click and movement
10 * - Keyboard input
11 * - Clipboard sync (bidirectional)
12 * - Connection status indicators
13 * - Auto-reconnect on disconnect
14 */
15
16import { useState, useEffect, useRef } from 'react';
17import {
18 Box,
19 Paper,
20 Alert,
21 Typography,
22 IconButton,
23 Tooltip,
24 CircularProgress,
25 TextField,
26 Button,
27 Stack
28} from '@mui/material';
29import {
30 Fullscreen as FullscreenIcon,
31 FullscreenExit as FullscreenExitIcon,
32 Refresh as RefreshIcon,
33 ContentCopy as CopyIcon,
34 ContentPaste as PasteIcon
35} from '@mui/icons-material';
36import { apiFetch } from '../lib/api';
37
38export default function RDSViewer({ agentId, agentUuid }) {
39 const [status, setStatus] = useState('idle'); // idle, starting, connecting, connected, error, disconnected
40 const [error, setError] = useState(null);
41 const [sessionId, setSessionId] = useState(null);
42 const [fullscreen, setFullscreen] = useState(false);
43 const [clipboard, setClipboard] = useState('');
44 const [agentClipboard, setAgentClipboard] = useState('');
45
46 const canvasRef = useRef(null);
47 const wsRef = useRef(null);
48 const containerRef = useRef(null);
49 const contextRef = useRef(null);
50 const imageRef = useRef(new Image());
51
52 // Start RDS session
53 const startSession = async () => {
54 try {
55 setStatus('starting');
56 setError(null);
57
58 // Request session from backend
59 const response = await apiFetch(`/rds/start/${agentUuid}`, {
60 method: 'POST'
61 });
62
63 setSessionId(response.sessionId);
64 setStatus('connecting');
65
66 // Connect to viewer WebSocket
67 connectWebSocket(response.sessionId);
68
69 } catch (err) {
70 console.error('[RDS] Failed to start session:', err);
71 setError(err.message || 'Failed to start remote desktop session');
72 setStatus('error');
73 }
74 };
75
76 // Connect to WebSocket viewer
77 const connectWebSocket = (sid) => {
78 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
79 const host = window.location.host;
80 const wsUrl = `${protocol}//${host}/api/rds/view/${sid}?session=${sid}`;
81
82 console.log('[RDS] Connecting to:', wsUrl);
83
84 const ws = new WebSocket(wsUrl);
85 ws.binaryType = 'arraybuffer';
86 wsRef.current = ws;
87
88 ws.onopen = () => {
89 console.log('[RDS] WebSocket connected');
90 setStatus('connected');
91 };
92
93 ws.onmessage = (event) => {
94 // Handle JSON messages (clipboard, status)
95 if (typeof event.data === 'string') {
96 try {
97 const msg = JSON.parse(event.data);
98 handleMessage(msg);
99 } catch (err) {
100 console.error('[RDS] Failed to parse message:', err);
101 }
102 return;
103 }
104
105 // Handle binary screen data (JPEG frames)
106 if (event.data instanceof ArrayBuffer) {
107 const blob = new Blob([event.data], { type: 'image/jpeg' });
108 const url = URL.createObjectURL(blob);
109
110 imageRef.current.onload = () => {
111 if (canvasRef.current && contextRef.current) {
112 const canvas = canvasRef.current;
113 const ctx = contextRef.current;
114
115 // Auto-resize canvas to match image
116 if (canvas.width !== imageRef.current.width || canvas.height !== imageRef.current.height) {
117 canvas.width = imageRef.current.width;
118 canvas.height = imageRef.current.height;
119 }
120
121 ctx.drawImage(imageRef.current, 0, 0);
122 }
123 URL.revokeObjectURL(url);
124 };
125
126 imageRef.current.src = url;
127 }
128 };
129
130 ws.onerror = (err) => {
131 console.error('[RDS] WebSocket error:', err);
132 setError('WebSocket connection error');
133 setStatus('error');
134 };
135
136 ws.onclose = () => {
137 console.log('[RDS] WebSocket closed');
138 setStatus('disconnected');
139 };
140 };
141
142 // Handle JSON messages (clipboard, status, etc.)
143 const handleMessage = (msg) => {
144 if (msg.type === 'clipboard') {
145 setAgentClipboard(msg.data || '');
146 } else if (msg.type === 'status') {
147 console.log('[RDS] Status update:', msg);
148 }
149 };
150
151 // Send mouse event
152 const handleMouse = (event) => {
153 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !sessionId) return;
154
155 const canvas = canvasRef.current;
156 const rect = canvas.getBoundingClientRect();
157 const x = Math.floor((event.clientX - rect.left) * (canvas.width / rect.width));
158 const y = Math.floor((event.clientY - rect.top) * (canvas.height / rect.height));
159
160 let button = 0;
161 if (event.type === 'mousedown' || event.type === 'mouseup' || event.type === 'click') {
162 button = event.button;
163 }
164
165 wsRef.current.send(JSON.stringify({
166 type: 'mouse',
167 sessionId: sessionId,
168 x: x,
169 y: y,
170 button: button,
171 action: event.type
172 }));
173 };
174
175 // Send keyboard event
176 const handleKeyboard = (event) => {
177 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !sessionId) return;
178
179 wsRef.current.send(JSON.stringify({
180 type: 'keyboard',
181 sessionId: sessionId,
182 key: event.key,
183 code: event.code,
184 action: event.type,
185 ctrlKey: event.ctrlKey,
186 shiftKey: event.shiftKey,
187 altKey: event.altKey,
188 metaKey: event.metaKey
189 }));
190 };
191
192 // Send clipboard to agent
193 const sendClipboard = () => {
194 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !sessionId) return;
195
196 wsRef.current.send(JSON.stringify({
197 type: 'clipboard',
198 sessionId: sessionId,
199 data: clipboard
200 }));
201
202 console.log('[RDS] Sent clipboard to agent');
203 };
204
205 // Copy agent clipboard to local
206 const copyAgentClipboard = async () => {
207 try {
208 await navigator.clipboard.writeText(agentClipboard);
209 console.log('[RDS] Copied agent clipboard to local');
210 } catch (err) {
211 console.error('[RDS] Failed to copy to clipboard:', err);
212 }
213 };
214
215 // Toggle fullscreen
216 const toggleFullscreen = () => {
217 if (!fullscreen) {
218 containerRef.current?.requestFullscreen();
219 setFullscreen(true);
220 } else {
221 document.exitFullscreen();
222 setFullscreen(false);
223 }
224 };
225
226 // Stop session
227 const stopSession = async () => {
228 if (wsRef.current) {
229 wsRef.current.close();
230 wsRef.current = null;
231 }
232
233 if (sessionId) {
234 try {
235 await apiFetch(`/rds/stop/${agentUuid}`, {
236 method: 'POST'
237 });
238 } catch (err) {
239 console.error('[RDS] Failed to stop session:', err);
240 }
241 setSessionId(null);
242 }
243
244 setStatus('idle');
245 };
246
247 // Initialize canvas context
248 useEffect(() => {
249 if (canvasRef.current) {
250 contextRef.current = canvasRef.current.getContext('2d');
251 }
252 }, []);
253
254 // Cleanup on unmount
255 useEffect(() => {
256 return () => {
257 stopSession();
258 };
259 }, []);
260
261 const statusColors = {
262 idle: 'default',
263 starting: 'info',
264 connecting: 'info',
265 connected: 'success',
266 error: 'error',
267 disconnected: 'warning'
268 };
269
270 const statusLabels = {
271 idle: 'Not connected',
272 starting: 'Starting session...',
273 connecting: 'Connecting...',
274 connected: 'Connected',
275 error: 'Error',
276 disconnected: 'Disconnected'
277 };
278
279 return (
280 <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
281 {/* Status Bar */}
282 <Alert severity={statusColors[status]} sx={{ mb: 2 }}>
283 Status: {statusLabels[status]}
284 {status === 'starting' && <CircularProgress size={16} sx={{ ml: 1 }} />}
285 </Alert>
286
287 {error && (
288 <Alert severity="error" sx={{ mb: 2 }}>
289 {error}
290 </Alert>
291 )}
292
293 {/* Control Bar */}
294 <Paper sx={{ p: 1, mb: 2, display: 'flex', gap: 1, alignItems: 'center' }}>
295 {status === 'idle' && (
296 <Button variant="contained" onClick={startSession}>
297 Start Remote Desktop
298 </Button>
299 )}
300
301 {status !== 'idle' && status !== 'starting' && (
302 <>
303 <Button variant="outlined" color="error" onClick={stopSession}>
304 Stop Session
305 </Button>
306
307 <Tooltip title="Refresh">
308 <IconButton onClick={() => { stopSession(); setTimeout(startSession, 1000); }}>
309 <RefreshIcon />
310 </IconButton>
311 </Tooltip>
312
313 <Tooltip title={fullscreen ? "Exit Fullscreen" : "Fullscreen"}>
314 <IconButton onClick={toggleFullscreen}>
315 {fullscreen ? <FullscreenExitIcon /> : <FullscreenIcon />}
316 </IconButton>
317 </Tooltip>
318 </>
319 )}
320 </Paper>
321
322 {/* Viewer Canvas */}
323 <Paper
324 ref={containerRef}
325 sx={{
326 flex: 1,
327 overflow: 'auto',
328 bgcolor: '#1e1e1e',
329 display: 'flex',
330 alignItems: 'center',
331 justifyContent: 'center',
332 position: 'relative'
333 }}
334 >
335 {status === 'idle' && (
336 <Typography color="text.secondary">
337 Click "Start Remote Desktop" to begin
338 </Typography>
339 )}
340
341 {(status === 'starting' || status === 'connecting') && (
342 <Box sx={{ textAlign: 'center' }}>
343 <CircularProgress />
344 <Typography color="text.secondary" sx={{ mt: 2 }}>
345 {statusLabels[status]}
346 </Typography>
347 </Box>
348 )}
349
350 {status === 'connected' && (
351 <canvas
352 ref={canvasRef}
353 onMouseDown={handleMouse}
354 onMouseUp={handleMouse}
355 onMouseMove={handleMouse}
356 onClick={handleMouse}
357 onKeyDown={handleKeyboard}
358 onKeyUp={handleKeyboard}
359 tabIndex={0}
360 style={{
361 maxWidth: '100%',
362 maxHeight: '100%',
363 cursor: 'crosshair',
364 outline: 'none'
365 }}
366 />
367 )}
368 </Paper>
369
370 {/* Clipboard Controls */}
371 {status === 'connected' && (
372 <Paper sx={{ p: 2, mt: 2 }}>
373 <Typography variant="h6" gutterBottom>
374 Clipboard Sync
375 </Typography>
376
377 <Stack spacing={2}>
378 {/* Send to Agent */}
379 <Box>
380 <Typography variant="subtitle2" gutterBottom>
381 Send to Remote Desktop
382 </Typography>
383 <Stack direction="row" spacing={1}>
384 <TextField
385 size="small"
386 fullWidth
387 multiline
388 rows={2}
389 value={clipboard}
390 onChange={(e) => setClipboard(e.target.value)}
391 placeholder="Type text to send to remote desktop"
392 />
393 <Button
394 variant="contained"
395 startIcon={<PasteIcon />}
396 onClick={sendClipboard}
397 disabled={!clipboard}
398 >
399 Send
400 </Button>
401 </Stack>
402 </Box>
403
404 {/* Receive from Agent */}
405 <Box>
406 <Typography variant="subtitle2" gutterBottom>
407 Received from Remote Desktop
408 </Typography>
409 <Stack direction="row" spacing={1}>
410 <TextField
411 size="small"
412 fullWidth
413 multiline
414 rows={2}
415 value={agentClipboard}
416 InputProps={{ readOnly: true }}
417 placeholder="Clipboard content from remote desktop will appear here"
418 />
419 <Button
420 variant="outlined"
421 startIcon={<CopyIcon />}
422 onClick={copyAgentClipboard}
423 disabled={!agentClipboard}
424 >
425 Copy
426 </Button>
427 </Stack>
428 </Box>
429 </Stack>
430 </Paper>
431 )}
432 </Box>
433 );
434}