@electerm/electerm-react 2.7.8 → 2.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/pre.js +38 -11
- package/client/components/bookmark-form/config/rdp.js +1 -0
- package/client/components/bookmark-form/config/vnc.js +5 -0
- package/client/components/common/remote-float-control.jsx +79 -0
- package/client/components/common/remote-float-control.styl +28 -0
- package/client/components/layout/layout.jsx +2 -1
- package/client/components/main/main.jsx +3 -6
- package/client/components/main/term-fullscreen.styl +1 -10
- package/client/components/rdp/rdp-session.jsx +131 -17
- package/client/components/rdp/resolutions.js +6 -0
- package/client/components/session/session.jsx +4 -3
- package/client/components/session/session.styl +18 -5
- package/client/components/session/sessions.jsx +2 -1
- package/client/components/shortcuts/shortcut-control.jsx +5 -3
- package/client/components/shortcuts/shortcut-handler.js +4 -2
- package/client/components/terminal/attach-addon-custom.js +13 -0
- package/client/components/terminal/event-emitter.js +27 -0
- package/client/components/terminal/terminal.jsx +10 -297
- package/client/components/terminal/zmodem-client.js +385 -0
- package/client/components/terminal-info/data-cols-parser.jsx +3 -2
- package/client/components/terminal-info/network.jsx +3 -2
- package/client/components/vnc/vnc-session.jsx +398 -62
- package/client/css/basic.styl +3 -0
- package/client/store/event.js +2 -2
- package/client/store/init-state.js +1 -1
- package/package.json +1 -1
- package/client/common/byte-format.js +0 -14
- package/client/components/main/term-fullscreen-control.jsx +0 -21
- package/client/components/terminal/xterm-zmodem.js +0 -55
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zmodem client handler for web terminal
|
|
3
|
+
* Handles UI interactions and communicates with server-side zmodem
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { throttle } from 'lodash-es'
|
|
7
|
+
import { filesize } from 'filesize'
|
|
8
|
+
import { transferTypeMap } from '../../common/constants.js'
|
|
9
|
+
import { getLocalFileInfo } from '../sftp/file-read.js'
|
|
10
|
+
|
|
11
|
+
const ZMODEM_SAVE_PATH_KEY = 'zmodem-save-path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ZmodemClient class handles zmodem UI and client-side logic
|
|
15
|
+
*/
|
|
16
|
+
export class ZmodemClient {
|
|
17
|
+
constructor (terminal) {
|
|
18
|
+
this.terminal = terminal
|
|
19
|
+
this.socket = null
|
|
20
|
+
this.isActive = false
|
|
21
|
+
this.currentTransfer = null
|
|
22
|
+
this.savePath = null
|
|
23
|
+
this.transferStartTime = 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize zmodem client with socket
|
|
28
|
+
* @param {WebSocket} socket - WebSocket connection
|
|
29
|
+
*/
|
|
30
|
+
init (socket) {
|
|
31
|
+
this.socket = socket
|
|
32
|
+
this.setupMessageHandler()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Setup message handler for zmodem events from server
|
|
37
|
+
*/
|
|
38
|
+
setupMessageHandler () {
|
|
39
|
+
if (!this.socket) return
|
|
40
|
+
|
|
41
|
+
// Use addEventListener to avoid conflicting with attach-addon's onmessage
|
|
42
|
+
this.messageHandler = (event) => {
|
|
43
|
+
// Check if it's a JSON message (zmodem control message)
|
|
44
|
+
if (typeof event.data === 'string') {
|
|
45
|
+
try {
|
|
46
|
+
const msg = JSON.parse(event.data)
|
|
47
|
+
if (msg.action === 'zmodem-event') {
|
|
48
|
+
this.handleServerEvent(msg)
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Not JSON, ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.socket.addEventListener('message', this.messageHandler)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle server zmodem events
|
|
60
|
+
* @param {Object} msg - Message from server
|
|
61
|
+
*/
|
|
62
|
+
handleServerEvent (msg) {
|
|
63
|
+
const { event } = msg
|
|
64
|
+
|
|
65
|
+
switch (event) {
|
|
66
|
+
case 'receive-start':
|
|
67
|
+
this.onReceiveStart()
|
|
68
|
+
break
|
|
69
|
+
case 'send-start':
|
|
70
|
+
this.onSendStart()
|
|
71
|
+
break
|
|
72
|
+
case 'file-start':
|
|
73
|
+
this.onFileStart(msg.name, msg.size)
|
|
74
|
+
break
|
|
75
|
+
case 'file-prepared':
|
|
76
|
+
this.onFilePrepared(msg.name, msg.path, msg.size)
|
|
77
|
+
break
|
|
78
|
+
case 'progress':
|
|
79
|
+
this.onProgress(msg)
|
|
80
|
+
break
|
|
81
|
+
case 'file-complete':
|
|
82
|
+
this.onFileComplete(msg.name, msg.path)
|
|
83
|
+
break
|
|
84
|
+
case 'file-skipped':
|
|
85
|
+
this.onFileSkipped(msg.name, msg.message)
|
|
86
|
+
break
|
|
87
|
+
case 'session-end':
|
|
88
|
+
this.onSessionEnd()
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send message to server
|
|
95
|
+
* @param {Object} msg - Message to send
|
|
96
|
+
*/
|
|
97
|
+
sendToServer (msg) {
|
|
98
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
99
|
+
this.socket.send(JSON.stringify({
|
|
100
|
+
action: 'zmodem-event',
|
|
101
|
+
...msg
|
|
102
|
+
}))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle receive session start
|
|
108
|
+
*/
|
|
109
|
+
async onReceiveStart () {
|
|
110
|
+
this.isActive = true
|
|
111
|
+
this.writeBanner('RECEIVE')
|
|
112
|
+
|
|
113
|
+
// Ask user for save directory
|
|
114
|
+
const savePath = await this.openSaveFolderSelect()
|
|
115
|
+
if (savePath) {
|
|
116
|
+
this.savePath = savePath
|
|
117
|
+
this.sendToServer({
|
|
118
|
+
event: 'set-save-path',
|
|
119
|
+
path: savePath
|
|
120
|
+
})
|
|
121
|
+
} else {
|
|
122
|
+
// User cancelled, end session
|
|
123
|
+
this.sendToServer({
|
|
124
|
+
event: 'cancel'
|
|
125
|
+
})
|
|
126
|
+
this.onSessionEnd()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle send session start
|
|
132
|
+
*/
|
|
133
|
+
async onSendStart () {
|
|
134
|
+
this.isActive = true
|
|
135
|
+
this.writeBanner('SEND')
|
|
136
|
+
|
|
137
|
+
// Ask user to select files
|
|
138
|
+
const files = await this.openFileSelect()
|
|
139
|
+
if (files && files.length > 0) {
|
|
140
|
+
this.sendToServer({
|
|
141
|
+
event: 'send-files',
|
|
142
|
+
files
|
|
143
|
+
})
|
|
144
|
+
} else {
|
|
145
|
+
// User cancelled, end session
|
|
146
|
+
this.sendToServer({
|
|
147
|
+
event: 'cancel'
|
|
148
|
+
})
|
|
149
|
+
this.onSessionEnd()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle file start event
|
|
155
|
+
* @param {string} name - File name
|
|
156
|
+
* @param {number} size - File size
|
|
157
|
+
*/
|
|
158
|
+
onFileStart (name, size) {
|
|
159
|
+
this.currentTransfer = {
|
|
160
|
+
name,
|
|
161
|
+
size,
|
|
162
|
+
transferred: 0,
|
|
163
|
+
type: this.savePath ? transferTypeMap.download : transferTypeMap.upload,
|
|
164
|
+
path: null
|
|
165
|
+
}
|
|
166
|
+
this.transferStartTime = Date.now()
|
|
167
|
+
this.writeProgress()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle file prepared event (for receive)
|
|
172
|
+
* @param {string} name - File name
|
|
173
|
+
* @param {string} path - File path
|
|
174
|
+
* @param {number} size - File size
|
|
175
|
+
*/
|
|
176
|
+
onFilePrepared (name, path, size) {
|
|
177
|
+
// Store the full path for display
|
|
178
|
+
if (this.currentTransfer) {
|
|
179
|
+
this.currentTransfer.path = path
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle progress update
|
|
185
|
+
* @param {Object} msg - Progress message
|
|
186
|
+
*/
|
|
187
|
+
onProgress (msg) {
|
|
188
|
+
if (!this.currentTransfer) {
|
|
189
|
+
this.currentTransfer = {
|
|
190
|
+
name: msg.name,
|
|
191
|
+
size: msg.size,
|
|
192
|
+
transferred: 0,
|
|
193
|
+
type: msg.type,
|
|
194
|
+
path: msg.path || null
|
|
195
|
+
}
|
|
196
|
+
this.transferStartTime = Date.now()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.currentTransfer.transferred = msg.transferred
|
|
200
|
+
this.currentTransfer.serverSpeed = msg.speed // Use server's speed calculation
|
|
201
|
+
this.currentTransfer.path = msg.path || this.currentTransfer.path
|
|
202
|
+
this.writeProgress()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle file complete
|
|
207
|
+
* @param {string} name - File name
|
|
208
|
+
* @param {string} path - File path
|
|
209
|
+
*/
|
|
210
|
+
onFileComplete (name, path) {
|
|
211
|
+
if (this.currentTransfer) {
|
|
212
|
+
this.currentTransfer.transferred = this.currentTransfer.size
|
|
213
|
+
this.currentTransfer.path = path
|
|
214
|
+
// Call directly to ensure 100% is displayed immediately
|
|
215
|
+
this._doWriteProgress(true)
|
|
216
|
+
// Add newline after completion
|
|
217
|
+
this.writeToTerminal('\r\n')
|
|
218
|
+
}
|
|
219
|
+
this.currentTransfer = null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handle file skipped (already exists on remote side)
|
|
224
|
+
* @param {string} name - File name
|
|
225
|
+
* @param {string} message - Skip message
|
|
226
|
+
*/
|
|
227
|
+
onFileSkipped (name, message) {
|
|
228
|
+
this.writeToTerminal(`\r\n\x1b[33m\x1b[1mSKIPPED: ${name} - ${message}\x1b[0m\r\n`)
|
|
229
|
+
this.currentTransfer = null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Handle session end
|
|
234
|
+
*/
|
|
235
|
+
onSessionEnd () {
|
|
236
|
+
this.isActive = false
|
|
237
|
+
this.currentTransfer = null
|
|
238
|
+
this.savePath = null
|
|
239
|
+
if (this.terminal && this.terminal.term) {
|
|
240
|
+
this.terminal.term.focus()
|
|
241
|
+
this.terminal.term.write('\r\n')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Write banner to terminal
|
|
247
|
+
* @param {string} type - 'RECEIVE' or 'SEND'
|
|
248
|
+
*/
|
|
249
|
+
writeBanner (type) {
|
|
250
|
+
const border = '='.repeat(50)
|
|
251
|
+
this.writeToTerminal(`\r\n${border}\r\n`)
|
|
252
|
+
this.writeToTerminal('\x1b[33m\x1b[1mRecommend use trzsz instead: https://github.com/trzsz/trzsz\x1b[0m\r\n')
|
|
253
|
+
this.writeToTerminal(`${border}\r\n\r\n`)
|
|
254
|
+
this.writeToTerminal(`\x1b[32m\x1b[1mZMODEM::${type}::START\x1b[0m\r\n`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Write progress to terminal (throttled for updates, immediate for completion)
|
|
259
|
+
* @param {boolean} isComplete - Whether this is the final completion display
|
|
260
|
+
*/
|
|
261
|
+
writeProgress = throttle((isComplete = false) => {
|
|
262
|
+
this._doWriteProgress(isComplete)
|
|
263
|
+
}, 500)
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Internal function to actually write progress
|
|
267
|
+
* @param {boolean} isComplete - Whether this is the final completion display
|
|
268
|
+
*/
|
|
269
|
+
_doWriteProgress (isComplete = false) {
|
|
270
|
+
if (!this.currentTransfer || !this.terminal?.term) return
|
|
271
|
+
|
|
272
|
+
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
273
|
+
const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
|
|
274
|
+
|
|
275
|
+
// Use server's speed if available, otherwise calculate locally
|
|
276
|
+
const speed = serverSpeed || 0
|
|
277
|
+
|
|
278
|
+
// Use full path if available, otherwise just name
|
|
279
|
+
const displayName = path || name
|
|
280
|
+
|
|
281
|
+
// filesize expects bytes and formats to human readable
|
|
282
|
+
const formatSize = (bytes) => filesize(bytes)
|
|
283
|
+
|
|
284
|
+
// Clear line and write progress
|
|
285
|
+
const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
|
|
286
|
+
this.writeToTerminal(str + '\r')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Write text to terminal
|
|
291
|
+
* @param {string} text - Text to write
|
|
292
|
+
*/
|
|
293
|
+
writeToTerminal (text) {
|
|
294
|
+
if (this.terminal && this.terminal.term) {
|
|
295
|
+
this.terminal.term.write(text)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Open file select dialog
|
|
301
|
+
* @returns {Promise<Array>} - Selected files
|
|
302
|
+
*/
|
|
303
|
+
openFileSelect = async () => {
|
|
304
|
+
const properties = [
|
|
305
|
+
'openFile',
|
|
306
|
+
'multiSelections',
|
|
307
|
+
'showHiddenFiles',
|
|
308
|
+
'noResolveAliases',
|
|
309
|
+
'treatPackageAsDirectory',
|
|
310
|
+
'dontAddToRecent'
|
|
311
|
+
]
|
|
312
|
+
const files = await window.api.openDialog({
|
|
313
|
+
title: 'Choose some files to send',
|
|
314
|
+
message: 'Choose some files to send',
|
|
315
|
+
properties
|
|
316
|
+
}).catch(() => false)
|
|
317
|
+
if (!files || !files.length) {
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
const r = []
|
|
321
|
+
for (const filePath of files) {
|
|
322
|
+
const stat = await getLocalFileInfo(filePath)
|
|
323
|
+
r.push({ ...stat, filePath, path: filePath })
|
|
324
|
+
}
|
|
325
|
+
return r
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Open save folder select dialog
|
|
330
|
+
* @returns {Promise<string>} - Selected folder path
|
|
331
|
+
*/
|
|
332
|
+
openSaveFolderSelect = async () => {
|
|
333
|
+
// Try to use last saved path
|
|
334
|
+
const lastPath = window.localStorage.getItem(ZMODEM_SAVE_PATH_KEY)
|
|
335
|
+
|
|
336
|
+
const savePaths = await window.api.openDialog({
|
|
337
|
+
title: 'Choose a folder to save file(s)',
|
|
338
|
+
message: 'Choose a folder to save file(s)',
|
|
339
|
+
defaultPath: lastPath || undefined,
|
|
340
|
+
properties: [
|
|
341
|
+
'openDirectory',
|
|
342
|
+
'showHiddenFiles',
|
|
343
|
+
'createDirectory',
|
|
344
|
+
'noResolveAliases',
|
|
345
|
+
'treatPackageAsDirectory',
|
|
346
|
+
'dontAddToRecent'
|
|
347
|
+
]
|
|
348
|
+
}).catch(() => false)
|
|
349
|
+
if (!savePaths || !savePaths.length) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
// Save for next time
|
|
353
|
+
window.localStorage.setItem(ZMODEM_SAVE_PATH_KEY, savePaths[0])
|
|
354
|
+
return savePaths[0]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Cancel ongoing transfer
|
|
359
|
+
*/
|
|
360
|
+
cancel () {
|
|
361
|
+
if (!this.isActive) return
|
|
362
|
+
this.writeToTerminal('\r\n\x1b[33m\x1b[1mZMODEM transfer cancelled by user\x1b[0m\r\n')
|
|
363
|
+
this.sendToServer({
|
|
364
|
+
event: 'cancel'
|
|
365
|
+
})
|
|
366
|
+
this.onSessionEnd()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Clean up resources
|
|
371
|
+
*/
|
|
372
|
+
destroy () {
|
|
373
|
+
if (this.socket && this.messageHandler) {
|
|
374
|
+
this.socket.removeEventListener('message', this.messageHandler)
|
|
375
|
+
this.messageHandler = null
|
|
376
|
+
}
|
|
377
|
+
this.isActive = false
|
|
378
|
+
this.currentTransfer = null
|
|
379
|
+
this.savePath = null
|
|
380
|
+
this.socket = null
|
|
381
|
+
this.terminal = null
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export default ZmodemClient
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copy } from '../../common/clipboard'
|
|
2
2
|
import filesizeParser from 'filesize-parser'
|
|
3
|
-
import {
|
|
3
|
+
import { filesize } from 'filesize'
|
|
4
4
|
|
|
5
5
|
const valueParserMaps = {
|
|
6
6
|
size: v => v,
|
|
@@ -23,7 +23,8 @@ function copyValue (event) {
|
|
|
23
23
|
export default (data) => {
|
|
24
24
|
return Object.keys(data).map(k => {
|
|
25
25
|
const rd = (txt) => {
|
|
26
|
-
|
|
26
|
+
// txt is in KB, convert to bytes for filesize function
|
|
27
|
+
const r = k === 'mem' ? filesize(parseInt(txt, 10) * 1024) : txt
|
|
27
28
|
const itemProps = {
|
|
28
29
|
className: 'activity-item pointer',
|
|
29
30
|
'data-content': r,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Table } from 'antd'
|
|
6
6
|
import { isEmpty } from 'lodash-es'
|
|
7
7
|
import { useEffect, useState } from 'react'
|
|
8
|
-
import {
|
|
8
|
+
import { filesize } from 'filesize'
|
|
9
9
|
import copy from 'json-deep-copy'
|
|
10
10
|
import { ApiOutlined } from '@ant-design/icons'
|
|
11
11
|
|
|
@@ -92,7 +92,8 @@ export default function TerminalInfoDisk (props) {
|
|
|
92
92
|
},
|
|
93
93
|
render: (v) => {
|
|
94
94
|
if (k === 'up' || k === 'down') {
|
|
95
|
-
|
|
95
|
+
console.log('render traffic', k, v)
|
|
96
|
+
return filesize(v || 0)
|
|
96
97
|
}
|
|
97
98
|
return v
|
|
98
99
|
}
|