@electerm/electerm-react 3.3.8 → 3.6.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/constants.js +0 -5
- package/client/common/fs.js +84 -0
- package/client/common/ws.js +16 -5
- package/client/components/ai/ai-history.jsx +0 -6
- package/client/components/batch-op/batch-op-alert.jsx +6 -25
- package/client/components/batch-op/batch-op-editor.jsx +9 -5
- package/client/components/bookmark-form/common/fields.jsx +15 -0
- package/client/components/bookmark-form/config/rdp.js +5 -0
- package/client/components/main/upgrade.jsx +133 -104
- package/client/components/main/upgrade.styl +2 -2
- package/client/components/rdp/file-transfer.js +375 -0
- package/client/components/rdp/rdp-session.jsx +169 -76
- package/client/components/rdp/rdp.styl +27 -0
- package/client/components/sftp/address-bar.jsx +16 -2
- package/client/components/shortcuts/shortcut-control.jsx +9 -0
- package/client/components/shortcuts/shortcuts-defaults.js +5 -0
- package/client/components/sidebar/info-modal.jsx +7 -2
- package/client/components/ssh-config/load-ssh-configs.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +2 -1
- package/client/store/app-upgrade.js +2 -2
- package/package.json +1 -1
|
@@ -8,23 +8,30 @@ import {
|
|
|
8
8
|
} from '../../common/constants'
|
|
9
9
|
import {
|
|
10
10
|
ReloadOutlined,
|
|
11
|
-
EditOutlined
|
|
11
|
+
EditOutlined,
|
|
12
|
+
UploadOutlined,
|
|
13
|
+
DownloadOutlined
|
|
12
14
|
} from '@ant-design/icons'
|
|
13
15
|
import {
|
|
14
16
|
Spin,
|
|
15
17
|
Select,
|
|
16
|
-
Switch
|
|
18
|
+
Switch,
|
|
19
|
+
Tooltip
|
|
17
20
|
} from 'antd'
|
|
18
21
|
import * as ls from '../../common/safe-local-storage'
|
|
19
22
|
import scanCode from './code-scan'
|
|
20
23
|
import resolutions from './resolutions'
|
|
21
24
|
import { readClipboardAsync } from '../../common/clipboard'
|
|
22
25
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
26
|
+
import HelpIcon from '../common/help-icon'
|
|
27
|
+
import { FileTransferManager, createFileLogger } from './file-transfer'
|
|
28
|
+
import { notification } from '../common/notification'
|
|
29
|
+
import message from '../common/message'
|
|
30
|
+
import ShowItem from '../common/show-item'
|
|
23
31
|
import './rdp.styl'
|
|
24
32
|
|
|
25
33
|
const { Option } = Select
|
|
26
34
|
|
|
27
|
-
// IronRDP WASM module imports — loaded dynamically
|
|
28
35
|
async function loadWasmModule () {
|
|
29
36
|
if (window.ironRdp) return
|
|
30
37
|
console.debug('[RDP-CLIENT] Loading IronRDP WASM module...')
|
|
@@ -55,9 +62,15 @@ export default class RdpSession extends PureComponent {
|
|
|
55
62
|
this.state = {
|
|
56
63
|
loading: false,
|
|
57
64
|
scaleViewport,
|
|
58
|
-
...resObj
|
|
65
|
+
...resObj,
|
|
66
|
+
hasRemoteFiles: false,
|
|
67
|
+
downloadBtnDisabled: true,
|
|
68
|
+
uploadReady: false
|
|
59
69
|
}
|
|
60
70
|
this.session = null
|
|
71
|
+
this.fileTransfer = null
|
|
72
|
+
this.fileUploadInProgress = false
|
|
73
|
+
this.log = createFileLogger()
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
componentDidMount () {
|
|
@@ -69,16 +82,18 @@ export default class RdpSession extends PureComponent {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
cleanup = () => {
|
|
72
|
-
console.debug('[RDP-CLIENT] cleanup() called')
|
|
73
85
|
if (this.session) {
|
|
74
86
|
try {
|
|
75
87
|
this.session.shutdown()
|
|
76
|
-
console.debug('[RDP-CLIENT] session.shutdown() called')
|
|
77
88
|
} catch (e) {
|
|
78
|
-
|
|
89
|
+
this.log(`session.shutdown() error: ${e.message}`, 'error')
|
|
79
90
|
}
|
|
80
91
|
this.session = null
|
|
81
92
|
}
|
|
93
|
+
if (this.fileTransfer) {
|
|
94
|
+
this.fileTransfer.cleanup()
|
|
95
|
+
this.fileTransfer = null
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
runInitScript = () => { }
|
|
@@ -126,7 +141,6 @@ export default class RdpSession extends PureComponent {
|
|
|
126
141
|
...tab
|
|
127
142
|
})
|
|
128
143
|
|
|
129
|
-
console.debug('[RDP-CLIENT] Creating RDP session term, host=', tab.host, 'port=', tab.port)
|
|
130
144
|
const r = await createTerm(opts)
|
|
131
145
|
.catch(err => {
|
|
132
146
|
const text = err.message
|
|
@@ -135,7 +149,6 @@ export default class RdpSession extends PureComponent {
|
|
|
135
149
|
if (!r) {
|
|
136
150
|
this.setState({ loading: false })
|
|
137
151
|
this.setStatus(statusMap.error)
|
|
138
|
-
console.error('[RDP-CLIENT] createTerm failed')
|
|
139
152
|
return
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -143,18 +156,15 @@ export default class RdpSession extends PureComponent {
|
|
|
143
156
|
pid, port
|
|
144
157
|
} = r
|
|
145
158
|
this.pid = pid
|
|
146
|
-
console.debug('[RDP-CLIENT] Term created, pid=', pid, 'port=', port)
|
|
147
159
|
|
|
148
|
-
// Build the WebSocket proxy address for IronRDP WASM
|
|
149
160
|
const { width, height } = this.state
|
|
150
|
-
// IronRDP connects to the proxy address, which then proxies via RDCleanPath
|
|
151
161
|
const extra = `&width=${width}&height=${height}`
|
|
152
162
|
const proxyAddress = this.buildWsUrl(port, 'rdp', extra)
|
|
153
|
-
|
|
163
|
+
|
|
154
164
|
try {
|
|
155
165
|
await loadWasmModule()
|
|
156
166
|
} catch (e) {
|
|
157
|
-
|
|
167
|
+
this.log(`Failed to load WASM module: ${e.message}`, 'error')
|
|
158
168
|
this.setState({ loading: false })
|
|
159
169
|
this.setStatus(statusMap.error)
|
|
160
170
|
return
|
|
@@ -162,11 +172,10 @@ export default class RdpSession extends PureComponent {
|
|
|
162
172
|
|
|
163
173
|
this.setStatus(statusMap.success)
|
|
164
174
|
|
|
165
|
-
// Connect using IronRDP SessionBuilder
|
|
166
175
|
try {
|
|
167
176
|
const canvas = this.canvasRef.current
|
|
168
177
|
if (!canvas) {
|
|
169
|
-
|
|
178
|
+
this.log('Canvas ref not available', 'error')
|
|
170
179
|
this.setState({ loading: false })
|
|
171
180
|
return
|
|
172
181
|
}
|
|
@@ -177,12 +186,6 @@ export default class RdpSession extends PureComponent {
|
|
|
177
186
|
const username = tab.username || ''
|
|
178
187
|
const password = tab.password || ''
|
|
179
188
|
|
|
180
|
-
console.debug('[RDP-CLIENT] Building IronRDP session...')
|
|
181
|
-
console.debug('[RDP-CLIENT] destination:', destination)
|
|
182
|
-
console.debug('[RDP-CLIENT] username:', username)
|
|
183
|
-
console.debug('[RDP-CLIENT] proxyAddress:', proxyAddress)
|
|
184
|
-
console.debug('[RDP-CLIENT] desktopSize:', width, 'x', height)
|
|
185
|
-
|
|
186
189
|
const desktopSize = new window.ironRdp.DesktopSize(width, height)
|
|
187
190
|
const enableCredsspExt = new window.ironRdp.Extension('enable_credssp', true)
|
|
188
191
|
|
|
@@ -196,7 +199,40 @@ export default class RdpSession extends PureComponent {
|
|
|
196
199
|
builder.renderCanvas(canvas)
|
|
197
200
|
builder.extension(enableCredsspExt)
|
|
198
201
|
|
|
199
|
-
|
|
202
|
+
this.fileTransfer = new FileTransferManager(
|
|
203
|
+
() => this.session,
|
|
204
|
+
this.log,
|
|
205
|
+
(inProgress) => {
|
|
206
|
+
this.fileUploadInProgress = inProgress
|
|
207
|
+
},
|
|
208
|
+
() => {
|
|
209
|
+
this.setState({ uploadReady: false })
|
|
210
|
+
},
|
|
211
|
+
(filePath, fileName, fileSize) => {
|
|
212
|
+
notification.success({
|
|
213
|
+
message: 'File downloaded from remote',
|
|
214
|
+
description: (
|
|
215
|
+
<ShowItem to={filePath}>
|
|
216
|
+
{`${fileName} (${this.fileTransfer.formatFileSize(fileSize)})`}
|
|
217
|
+
</ShowItem>
|
|
218
|
+
),
|
|
219
|
+
duration: 0
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
this.fileTransfer.setStateChangeCallback((state) => {
|
|
225
|
+
this.setState({
|
|
226
|
+
hasRemoteFiles: state.hasRemoteFiles,
|
|
227
|
+
downloadBtnDisabled: !state.hasRemoteFiles
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const fileTransferExtensions = this.fileTransfer.createExtensions()
|
|
232
|
+
fileTransferExtensions.forEach((ext) => {
|
|
233
|
+
builder.extension(ext)
|
|
234
|
+
})
|
|
235
|
+
|
|
200
236
|
builder.remoteClipboardChangedCallback((clipboardData) => {
|
|
201
237
|
try {
|
|
202
238
|
if (clipboardData.isEmpty()) {
|
|
@@ -204,14 +240,14 @@ export default class RdpSession extends PureComponent {
|
|
|
204
240
|
}
|
|
205
241
|
const items = clipboardData.items()
|
|
206
242
|
for (const item of items) {
|
|
207
|
-
|
|
243
|
+
const mimeType = item.mimeType()
|
|
244
|
+
if (mimeType === 'text/plain') {
|
|
208
245
|
const text = item.value()
|
|
209
|
-
console.debug('[RDP-CLIENT] Received clipboard text:', text)
|
|
210
246
|
window.pre.writeClipboard(text)
|
|
211
247
|
}
|
|
212
248
|
}
|
|
213
249
|
} catch (e) {
|
|
214
|
-
|
|
250
|
+
this.log(`Clipboard error: ${e.message}`, 'error')
|
|
215
251
|
}
|
|
216
252
|
})
|
|
217
253
|
|
|
@@ -219,19 +255,15 @@ export default class RdpSession extends PureComponent {
|
|
|
219
255
|
this.syncLocalToRemote()
|
|
220
256
|
})
|
|
221
257
|
|
|
222
|
-
// Cursor style callback
|
|
223
258
|
builder.setCursorStyleCallbackContext(canvas)
|
|
224
259
|
builder.setCursorStyleCallback(function (style) {
|
|
225
260
|
canvas.style.cursor = style || 'default'
|
|
226
261
|
})
|
|
227
262
|
|
|
228
|
-
console.debug('[RDP-CLIENT] Calling builder.connect()...')
|
|
229
263
|
this.session = await builder.connect()
|
|
230
264
|
|
|
231
265
|
const ds = this.session.desktopSize()
|
|
232
|
-
console.debug('[RDP-CLIENT] Connected! Desktop:', ds.width, 'x', ds.height)
|
|
233
266
|
|
|
234
|
-
// Update canvas size to match actual desktop size
|
|
235
267
|
canvas.width = ds.width
|
|
236
268
|
canvas.height = ds.height
|
|
237
269
|
|
|
@@ -241,17 +273,15 @@ export default class RdpSession extends PureComponent {
|
|
|
241
273
|
|
|
242
274
|
canvas.focus()
|
|
243
275
|
|
|
244
|
-
// Run the session event loop (renders frames, handles protocol)
|
|
245
|
-
console.debug('[RDP-CLIENT] Starting session.run() event loop')
|
|
246
276
|
this.session.run().then((info) => {
|
|
247
|
-
|
|
277
|
+
this.log(`Session ended: ${info.reason()}`, 'info')
|
|
248
278
|
this.onSessionEnd()
|
|
249
279
|
}).catch((e) => {
|
|
250
|
-
|
|
280
|
+
this.log(`Session error: ${this.formatError(e)}`, 'error')
|
|
251
281
|
this.onSessionEnd()
|
|
252
282
|
})
|
|
253
283
|
} catch (e) {
|
|
254
|
-
|
|
284
|
+
this.log(`Connection failed: ${this.formatError(e)}`, 'error')
|
|
255
285
|
this.setState({ loading: false })
|
|
256
286
|
this.setStatus(statusMap.error)
|
|
257
287
|
}
|
|
@@ -278,7 +308,9 @@ export default class RdpSession extends PureComponent {
|
|
|
278
308
|
}
|
|
279
309
|
|
|
280
310
|
syncLocalToRemote = async () => {
|
|
281
|
-
if (!this.session)
|
|
311
|
+
if (!this.session || this.fileUploadInProgress) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
282
314
|
try {
|
|
283
315
|
const text = await readClipboardAsync()
|
|
284
316
|
if (text) {
|
|
@@ -287,12 +319,11 @@ export default class RdpSession extends PureComponent {
|
|
|
287
319
|
await this.session.onClipboardPaste(data)
|
|
288
320
|
}
|
|
289
321
|
} catch (e) {
|
|
290
|
-
|
|
322
|
+
this.log(`Local clipboard sync error: ${e.message}`, 'error')
|
|
291
323
|
}
|
|
292
324
|
}
|
|
293
325
|
|
|
294
326
|
onSessionEnd = () => {
|
|
295
|
-
console.debug('[RDP-CLIENT] onSessionEnd called')
|
|
296
327
|
this.session = null
|
|
297
328
|
this.setStatus(statusMap.error)
|
|
298
329
|
}
|
|
@@ -301,7 +332,6 @@ export default class RdpSession extends PureComponent {
|
|
|
301
332
|
const canvas = this.canvasRef.current
|
|
302
333
|
if (!canvas || this._inputHandlersSetup) return
|
|
303
334
|
this._inputHandlersSetup = true
|
|
304
|
-
console.debug('[RDP-CLIENT] Setting up input handlers')
|
|
305
335
|
|
|
306
336
|
canvas.addEventListener('keydown', (e) => {
|
|
307
337
|
e.preventDefault()
|
|
@@ -315,7 +345,7 @@ export default class RdpSession extends PureComponent {
|
|
|
315
345
|
tx.addEvent(event)
|
|
316
346
|
this.session.applyInputs(tx)
|
|
317
347
|
} catch (err) {
|
|
318
|
-
|
|
348
|
+
this.log(`Key press error: ${err.message}`, 'error')
|
|
319
349
|
}
|
|
320
350
|
})
|
|
321
351
|
|
|
@@ -331,7 +361,7 @@ export default class RdpSession extends PureComponent {
|
|
|
331
361
|
tx.addEvent(event)
|
|
332
362
|
this.session.applyInputs(tx)
|
|
333
363
|
} catch (err) {
|
|
334
|
-
|
|
364
|
+
this.log(`Key release error: ${err.message}`, 'error')
|
|
335
365
|
}
|
|
336
366
|
})
|
|
337
367
|
|
|
@@ -381,7 +411,7 @@ export default class RdpSession extends PureComponent {
|
|
|
381
411
|
tx.addEvent(event)
|
|
382
412
|
this.session.applyInputs(tx)
|
|
383
413
|
} catch (err) {
|
|
384
|
-
|
|
414
|
+
this.log(`Mouse down error: ${err.message}`, 'error')
|
|
385
415
|
}
|
|
386
416
|
})
|
|
387
417
|
|
|
@@ -394,7 +424,7 @@ export default class RdpSession extends PureComponent {
|
|
|
394
424
|
tx.addEvent(event)
|
|
395
425
|
this.session.applyInputs(tx)
|
|
396
426
|
} catch (err) {
|
|
397
|
-
|
|
427
|
+
this.log(`Mouse up error: ${err.message}`, 'error')
|
|
398
428
|
}
|
|
399
429
|
})
|
|
400
430
|
|
|
@@ -417,14 +447,15 @@ export default class RdpSession extends PureComponent {
|
|
|
417
447
|
this.session.applyInputs(tx)
|
|
418
448
|
}
|
|
419
449
|
} catch (err) {
|
|
420
|
-
|
|
450
|
+
this.log(`Wheel error: ${err.message}`, 'error')
|
|
421
451
|
}
|
|
422
452
|
}, { passive: false })
|
|
423
453
|
|
|
424
454
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
425
455
|
|
|
426
|
-
canvas.addEventListener('paste', () => {
|
|
427
|
-
|
|
456
|
+
canvas.addEventListener('paste', (e) => {
|
|
457
|
+
e.preventDefault()
|
|
458
|
+
this.handlePasteEvent()
|
|
428
459
|
})
|
|
429
460
|
|
|
430
461
|
canvas.addEventListener('focus', () => {
|
|
@@ -432,7 +463,6 @@ export default class RdpSession extends PureComponent {
|
|
|
432
463
|
})
|
|
433
464
|
}
|
|
434
465
|
|
|
435
|
-
// Get PS/2 scancode from keyboard event code using existing code-scan module
|
|
436
466
|
getScancode = (code) => {
|
|
437
467
|
const sc = scanCode({ code })
|
|
438
468
|
return sc !== undefined ? sc : null
|
|
@@ -443,7 +473,6 @@ export default class RdpSession extends PureComponent {
|
|
|
443
473
|
}
|
|
444
474
|
|
|
445
475
|
handleReInit = () => {
|
|
446
|
-
console.debug('[RDP-CLIENT] handleReInit called')
|
|
447
476
|
this.cleanup()
|
|
448
477
|
this.props.reloadTab(
|
|
449
478
|
this.props.tab
|
|
@@ -492,9 +521,9 @@ export default class RdpSession extends PureComponent {
|
|
|
492
521
|
return {
|
|
493
522
|
isFullScreen: this.props.fullscreen,
|
|
494
523
|
onSendCtrlAltDel: this.handleSendCtrlAltDel,
|
|
495
|
-
screens: [],
|
|
524
|
+
screens: [],
|
|
496
525
|
currentScreen: null,
|
|
497
|
-
onSelectScreen: () => { },
|
|
526
|
+
onSelectScreen: () => { },
|
|
498
527
|
fixedPosition,
|
|
499
528
|
showExitFullscreen,
|
|
500
529
|
className
|
|
@@ -504,37 +533,87 @@ export default class RdpSession extends PureComponent {
|
|
|
504
533
|
handleSendCtrlAltDel = () => {
|
|
505
534
|
if (!this.session) return
|
|
506
535
|
try {
|
|
507
|
-
// Send Ctrl+Alt+Del sequence using IronRDP
|
|
508
536
|
const tx = new window.ironRdp.InputTransaction()
|
|
509
|
-
|
|
510
|
-
// Ctrl key press
|
|
511
|
-
const ctrlScancode = 0x1D // Left Ctrl scancode
|
|
537
|
+
const ctrlScancode = 0x1D
|
|
512
538
|
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(ctrlScancode))
|
|
513
|
-
|
|
514
|
-
// Alt key press
|
|
515
|
-
const altScancode = 0x38 // Left Alt scancode
|
|
539
|
+
const altScancode = 0x38
|
|
516
540
|
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(altScancode))
|
|
517
|
-
|
|
518
|
-
// Del key press
|
|
519
|
-
const delScancode = 0x53 // Delete scancode
|
|
541
|
+
const delScancode = 0x53
|
|
520
542
|
tx.addEvent(window.ironRdp.DeviceEvent.keyPressed(delScancode))
|
|
521
|
-
|
|
522
|
-
// Del key release
|
|
523
543
|
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(delScancode))
|
|
524
|
-
|
|
525
|
-
// Alt key release
|
|
526
544
|
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(altScancode))
|
|
527
|
-
|
|
528
|
-
// Ctrl key release
|
|
529
545
|
tx.addEvent(window.ironRdp.DeviceEvent.keyReleased(ctrlScancode))
|
|
530
|
-
|
|
531
546
|
this.session.applyInputs(tx)
|
|
532
|
-
console.log('[RDP-CLIENT] Sent Ctrl+Alt+Del')
|
|
533
547
|
} catch (err) {
|
|
534
|
-
|
|
548
|
+
this.log(`Failed to send Ctrl+Alt+Del: ${err.message}`, 'error')
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
|
|
552
|
+
handleUploadButtonClick = async () => {
|
|
553
|
+
const properties = [
|
|
554
|
+
'openFile',
|
|
555
|
+
'multiSelections',
|
|
556
|
+
'showHiddenFiles',
|
|
557
|
+
'noResolveAliases',
|
|
558
|
+
'treatPackageAsDirectory',
|
|
559
|
+
'dontAddToRecent'
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
const files = await window.api.openDialog({
|
|
563
|
+
title: 'Choose files to upload to remote desktop',
|
|
564
|
+
message: 'Choose files to upload',
|
|
565
|
+
properties
|
|
566
|
+
}).catch((err) => {
|
|
567
|
+
this.log(`File dialog error: ${err.message}`, 'error')
|
|
568
|
+
return false
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
if (!files || !files.length) {
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
message.info('Ready to paste on remote to upload files', 5)
|
|
576
|
+
this.setState({ uploadReady: true })
|
|
577
|
+
|
|
578
|
+
if (this.fileTransfer) {
|
|
579
|
+
await this.fileTransfer.uploadFromPaths(files)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
handleDownloadButtonClick = () => {
|
|
584
|
+
if (this.fileTransfer) {
|
|
585
|
+
this.fileTransfer.downloadFiles()
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
handlePasteEvent = async () => {
|
|
590
|
+
const text = await readClipboardAsync()
|
|
591
|
+
|
|
592
|
+
if (!text) {
|
|
593
|
+
this.syncLocalToRemote()
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const fileRegWin = /^\w:\\.+/
|
|
598
|
+
const fileReg = /^\/.+/
|
|
599
|
+
const lines = text.split('\n')
|
|
600
|
+
|
|
601
|
+
const filePaths = lines.filter(line => fileReg.test(line) || fileRegWin.test(line))
|
|
602
|
+
|
|
603
|
+
if (filePaths.length === 0) {
|
|
604
|
+
this.syncLocalToRemote()
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (this.fileTransfer) {
|
|
609
|
+
await this.fileTransfer.uploadFromPaths(filePaths)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
componentDidUpdate () {
|
|
614
|
+
this.setupInputHandlers()
|
|
615
|
+
}
|
|
616
|
+
|
|
538
617
|
renderControl = () => {
|
|
539
618
|
const contrlProps = this.getControlProps({
|
|
540
619
|
fixedPosition: false,
|
|
@@ -542,7 +621,9 @@ export default class RdpSession extends PureComponent {
|
|
|
542
621
|
className: 'mg1l'
|
|
543
622
|
})
|
|
544
623
|
const {
|
|
545
|
-
id
|
|
624
|
+
id,
|
|
625
|
+
hasRemoteFiles,
|
|
626
|
+
uploadReady
|
|
546
627
|
} = this.state
|
|
547
628
|
const sleProps = {
|
|
548
629
|
value: id,
|
|
@@ -556,6 +637,8 @@ export default class RdpSession extends PureComponent {
|
|
|
556
637
|
checkedChildren: window.translate('scaleViewport'),
|
|
557
638
|
className: 'mg1l'
|
|
558
639
|
}
|
|
640
|
+
const uploadTitle = window.translate('upload') || 'Upload files to remote'
|
|
641
|
+
const downloadTitle = window.translate('download') || 'Download files from remote'
|
|
559
642
|
return (
|
|
560
643
|
<div
|
|
561
644
|
className='pd1 fix session-v-info block'
|
|
@@ -587,10 +670,25 @@ export default class RdpSession extends PureComponent {
|
|
|
587
670
|
className='mg2r mg1l pointer'
|
|
588
671
|
/>
|
|
589
672
|
{this.renderInfo()}
|
|
590
|
-
{this.renderHelp()}
|
|
591
673
|
<Switch
|
|
592
674
|
{...scaleProps}
|
|
593
675
|
/>
|
|
676
|
+
<Tooltip title={uploadTitle}>
|
|
677
|
+
<UploadOutlined
|
|
678
|
+
onClick={this.handleUploadButtonClick}
|
|
679
|
+
className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
|
|
680
|
+
/>
|
|
681
|
+
</Tooltip>
|
|
682
|
+
<Tooltip title={downloadTitle}>
|
|
683
|
+
<DownloadOutlined
|
|
684
|
+
onClick={this.handleDownloadButtonClick}
|
|
685
|
+
className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
|
|
686
|
+
/>
|
|
687
|
+
</Tooltip>
|
|
688
|
+
<HelpIcon
|
|
689
|
+
link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
|
|
690
|
+
className='mg2r mg1l'
|
|
691
|
+
/>
|
|
594
692
|
</div>
|
|
595
693
|
<div className='fright'>
|
|
596
694
|
{this.props.fullscreenIcon()}
|
|
@@ -613,11 +711,6 @@ export default class RdpSession extends PureComponent {
|
|
|
613
711
|
)
|
|
614
712
|
}
|
|
615
713
|
|
|
616
|
-
componentDidUpdate () {
|
|
617
|
-
// Set up native input handlers after canvas is rendered
|
|
618
|
-
this.setupInputHandlers()
|
|
619
|
-
}
|
|
620
|
-
|
|
621
714
|
render () {
|
|
622
715
|
const { width: w, height: h } = this.props
|
|
623
716
|
const { width, height, loading, scaleViewport } = this.state
|
|
@@ -32,4 +32,31 @@
|
|
|
32
32
|
background var(--main)
|
|
33
33
|
z-index 299
|
|
34
34
|
|
|
35
|
+
.rdp-file-transfer-btn
|
|
36
|
+
font-size: 16px
|
|
37
|
+
color: var(--text)
|
|
38
|
+
transition: color 0.2s
|
|
39
|
+
&:hover
|
|
40
|
+
color: var(--primary)
|
|
41
|
+
|
|
42
|
+
.rdp-download-disabled
|
|
43
|
+
opacity: 0.4
|
|
44
|
+
cursor: not-allowed
|
|
45
|
+
&:hover
|
|
46
|
+
color: var(--text)
|
|
47
|
+
|
|
48
|
+
canvas
|
|
49
|
+
outline: none
|
|
50
|
+
|
|
51
|
+
@keyframes rdp-download-flash
|
|
52
|
+
0%, 100%
|
|
53
|
+
color: var(--primary)
|
|
54
|
+
transform: scale(1)
|
|
55
|
+
50%
|
|
56
|
+
color: var(--success)
|
|
57
|
+
transform: scale(1.2)
|
|
58
|
+
|
|
59
|
+
.rdp-download-flash
|
|
60
|
+
animation: rdp-download-flash 1s ease-in-out infinite
|
|
61
|
+
|
|
35
62
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
ArrowUpOutlined,
|
|
3
4
|
EyeInvisibleFilled,
|
|
@@ -134,15 +135,28 @@ export default function AddressBar (props) {
|
|
|
134
135
|
const GoIcon = isLoadingRemote
|
|
135
136
|
? LoadingOutlined
|
|
136
137
|
: (realPath === path ? ReloadOutlined : ArrowRightOutlined)
|
|
138
|
+
const inputRef = useRef(null)
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const wrapEl = inputRef.current
|
|
142
|
+
if (!wrapEl) return
|
|
143
|
+
const inputEl = wrapEl.querySelector('input')
|
|
144
|
+
if (!inputEl) return
|
|
145
|
+
const handler = () => props.onInputFocus(type)
|
|
146
|
+
inputEl.addEventListener('click', handler)
|
|
147
|
+
return () => {
|
|
148
|
+
inputEl.removeEventListener('click', handler)
|
|
149
|
+
}
|
|
150
|
+
}, [type])
|
|
151
|
+
|
|
137
152
|
return (
|
|
138
153
|
<div className='pd1y sftp-title-wrap'>
|
|
139
|
-
<div className='sftp-title'>
|
|
154
|
+
<div className='sftp-title' ref={inputRef}>
|
|
140
155
|
<Input
|
|
141
156
|
value={path}
|
|
142
157
|
onChange={e => props.onChange(e, n)}
|
|
143
158
|
onPressEnter={e => props.onGoto(type, e)}
|
|
144
159
|
prefix={renderAddonBefore(props, realPath)}
|
|
145
|
-
onFocus={() => props.onInputFocus(type)}
|
|
146
160
|
onBlur={() => props.onInputBlur(type)}
|
|
147
161
|
disabled={loadingSftp}
|
|
148
162
|
suffix={
|
|
@@ -172,6 +172,15 @@ class ShortcutControl extends React.PureComponent {
|
|
|
172
172
|
window.store.onNewSsh()
|
|
173
173
|
}, 500)
|
|
174
174
|
|
|
175
|
+
newTabShortcut = throttle((e) => {
|
|
176
|
+
e.stopPropagation()
|
|
177
|
+
if (window.store.hasNodePty) {
|
|
178
|
+
window.store.addTab()
|
|
179
|
+
} else {
|
|
180
|
+
window.store.onNewSsh()
|
|
181
|
+
}
|
|
182
|
+
}, 500)
|
|
183
|
+
|
|
175
184
|
toggleAddBtnShortcut = throttle((e) => {
|
|
176
185
|
e.stopPropagation()
|
|
177
186
|
const { currentLayoutBatch } = window.store
|
|
@@ -42,8 +42,10 @@ export default auto(function InfoModal (props) {
|
|
|
42
42
|
upgradeInfo
|
|
43
43
|
} = props
|
|
44
44
|
const onCheckUpdating = upgradeInfo.checkingRemoteVersion || upgradeInfo.upgrading
|
|
45
|
+
const { noUpdateMessage, noUpdateMessageExpires } = upgradeInfo
|
|
46
|
+
const showMessage = noUpdateMessage && noUpdateMessageExpires && Date.now() < noUpdateMessageExpires
|
|
45
47
|
return (
|
|
46
|
-
<
|
|
48
|
+
<div className='mg1b mg2t'>
|
|
47
49
|
<Button
|
|
48
50
|
type='primary'
|
|
49
51
|
loading={onCheckUpdating}
|
|
@@ -51,7 +53,10 @@ export default auto(function InfoModal (props) {
|
|
|
51
53
|
>
|
|
52
54
|
{e('checkForUpdate')}
|
|
53
55
|
</Button>
|
|
54
|
-
|
|
56
|
+
{showMessage && (
|
|
57
|
+
<span className='mg1l update-msg'>{noUpdateMessage}</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
55
60
|
)
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Modal,
|
|
3
2
|
Spin,
|
|
4
3
|
Button,
|
|
5
4
|
Empty
|
|
@@ -9,6 +8,7 @@ import * as ls from '../../common/safe-local-storage'
|
|
|
9
8
|
import {
|
|
10
9
|
sshConfigLoadKey
|
|
11
10
|
} from '../../common/constants'
|
|
11
|
+
import Modal from '../common/modal'
|
|
12
12
|
import { ReloadOutlined } from '@ant-design/icons'
|
|
13
13
|
import LoadSshConfigsItem from './load-ssh-configs-item'
|
|
14
14
|
import './ssh-config.styl'
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { refsStatic } from '../components/common/ref'
|
|
6
6
|
|
|
7
7
|
export default Store => {
|
|
8
|
-
Store.prototype.onCheckUpdate = (
|
|
9
|
-
refsStatic.get('upgrade')?.appUpdateCheck(
|
|
8
|
+
Store.prototype.onCheckUpdate = (isManual = false) => {
|
|
9
|
+
refsStatic.get('upgrade')?.appUpdateCheck(isManual)
|
|
10
10
|
}
|
|
11
11
|
Store.prototype.getProxySetting = function () {
|
|
12
12
|
const {
|