@electerm/electerm-react 2.11.16 → 2.13.0

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.
@@ -32,6 +32,7 @@ export default {
32
32
  enableSixel: true,
33
33
  terminalType: 'xterm-256color',
34
34
  keepaliveCountMax: 10,
35
+ keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode',
35
36
  saveTerminalLogToFile: false,
36
37
  checkUpdateOnStart: true,
37
38
  cursorBlink: false,
@@ -29,7 +29,10 @@ export const getFilePath = (file) => {
29
29
  export const getDropFileList = (dataTransfer) => {
30
30
  const fromFile = dataTransfer.getData('fromFile')
31
31
  if (fromFile) {
32
- return [JSON.parse(fromFile)]
32
+ const parsed = JSON.parse(fromFile)
33
+ return Array.isArray(parsed)
34
+ ? parsed
35
+ : [parsed]
33
36
  }
34
37
 
35
38
  const { files } = dataTransfer
@@ -0,0 +1,176 @@
1
+ import { autoRun } from 'manate'
2
+ import copy from 'json-deep-copy'
3
+ import uid from '../../common/uid'
4
+ import resolve from '../../common/resolve'
5
+ import fs from '../../common/fs'
6
+ import { typeMap } from '../../common/constants'
7
+ import { getFolderFromFilePath, getLocalFileInfo } from '../sftp/file-read'
8
+
9
+ export default class Remote2RemoteHandler {
10
+ constructor (props) {
11
+ this.props = props
12
+ this.id = uid()
13
+ }
14
+
15
+ get store () {
16
+ return window.store
17
+ }
18
+
19
+ get fromFile () {
20
+ return this.props.fromFile
21
+ }
22
+
23
+ get fromPath () {
24
+ const { path, name } = this.fromFile
25
+ return resolve(path, name)
26
+ }
27
+
28
+ get toPath () {
29
+ return this.props.toPath
30
+ }
31
+
32
+ buildTempPath = () => {
33
+ const { name, ext, base } = getFolderFromFilePath(this.fromPath, true)
34
+ const tail = uid()
35
+ const tempName = ext
36
+ ? `${base}-${tail}.${ext}`
37
+ : `${name}-${tail}`
38
+ return resolve(window.pre.tempDir, tempName)
39
+ }
40
+
41
+ buildStep1Transfer = () => {
42
+ const {
43
+ title,
44
+ tabType,
45
+ sourceTabId,
46
+ sourceHost
47
+ } = this.props
48
+ const transfer = {
49
+ id: uid(),
50
+ typeFrom: typeMap.remote,
51
+ typeTo: typeMap.local,
52
+ fromPath: this.fromPath,
53
+ toPath: this.tempPath,
54
+ tabId: sourceTabId,
55
+ host: sourceHost,
56
+ title,
57
+ tabType,
58
+ operation: '',
59
+ remote2remoteStep: 1,
60
+ remote2remoteId: this.id
61
+ }
62
+ return transfer
63
+ }
64
+
65
+ buildStep2Transfer = (fromFile) => {
66
+ const {
67
+ targetTabId,
68
+ targetHost,
69
+ targetTitle,
70
+ targetTabType
71
+ } = this.props
72
+ const transfer = {
73
+ id: uid(),
74
+ typeFrom: typeMap.local,
75
+ typeTo: typeMap.remote,
76
+ fromPath: this.tempPath,
77
+ toPath: this.toPath,
78
+ fromFile,
79
+ tabId: targetTabId,
80
+ host: targetHost,
81
+ title: targetTitle,
82
+ tabType: targetTabType,
83
+ operation: '',
84
+ remote2remoteStep: 2,
85
+ remote2remoteId: this.id,
86
+ originalId: this.step1Transfer?.id
87
+ }
88
+ return transfer
89
+ }
90
+
91
+ start = () => {
92
+ this.tempPath = this.buildTempPath()
93
+ this.step1Transfer = this.buildStep1Transfer()
94
+ this.store.addTransferList([copy(this.step1Transfer)])
95
+ this.startWatch()
96
+ }
97
+
98
+ startWatch = () => {
99
+ this.ref = autoRun(() => {
100
+ this.tick()
101
+ return this.store.transferHistory
102
+ })
103
+ this.ref.start()
104
+ }
105
+
106
+ stopWatch = () => {
107
+ this.ref?.stop()
108
+ this.ref = null
109
+ }
110
+
111
+ tick = async () => {
112
+ const step1 = this.findHistory(this.step1Transfer?.id)
113
+
114
+ if (!this.step2Transfer) {
115
+ if (this.creatingStep2) {
116
+ return
117
+ }
118
+ if (!step1) {
119
+ return
120
+ }
121
+ if (step1.error) {
122
+ return this.finish(step1.error)
123
+ }
124
+ this.creatingStep2 = true
125
+ const localFromFile = await getLocalFileInfo(this.tempPath).catch(() => null)
126
+ if (!localFromFile) {
127
+ this.creatingStep2 = false
128
+ return this.finish('local temp file/folder not found')
129
+ }
130
+ this.step2Transfer = this.buildStep2Transfer(localFromFile)
131
+ this.creatingStep2 = false
132
+ this.store.addTransferList([copy(this.step2Transfer)])
133
+ return
134
+ }
135
+
136
+ const step2 = this.findHistory(this.step2Transfer.id)
137
+ if (!step2) {
138
+ return
139
+ }
140
+
141
+ return this.finish(step2.error)
142
+ }
143
+
144
+ findHistory = (transferId) => {
145
+ if (!transferId) {
146
+ return null
147
+ }
148
+ return this.store.transferHistory.find(item => {
149
+ return item.id === transferId || item.originalId === transferId
150
+ })
151
+ }
152
+
153
+ cleanup = async () => {
154
+ if (!this.tempPath) {
155
+ return
156
+ }
157
+ await fs.rmrf(this.tempPath).catch(() => {})
158
+ }
159
+
160
+ finish = async (error) => {
161
+ if (this.finished) {
162
+ return
163
+ }
164
+ this.finished = true
165
+ this.stopWatch()
166
+ await this.cleanup()
167
+ this.props.onDone?.({
168
+ id: this.id,
169
+ error
170
+ })
171
+ }
172
+
173
+ stop = async () => {
174
+ await this.finish()
175
+ }
176
+ }
@@ -0,0 +1,81 @@
1
+ import { Component } from 'react'
2
+ import resolve from '../../common/resolve'
3
+ import { typeMap } from '../../common/constants'
4
+ import { refsStatic } from '../common/ref'
5
+ import Remote2RemoteHandler from './remote2remote-handler'
6
+
7
+ const handlerRefId = 'remote2remote-handlers'
8
+
9
+ export default class Remote2RemoteHandlers extends Component {
10
+ constructor (props) {
11
+ super(props)
12
+ this.handlers = new Map()
13
+ }
14
+
15
+ componentDidMount () {
16
+ refsStatic.add(handlerRefId, this)
17
+ }
18
+
19
+ componentWillUnmount () {
20
+ refsStatic.remove(handlerRefId)
21
+ this.handlers.forEach(handler => {
22
+ handler.stop()
23
+ })
24
+ this.handlers.clear()
25
+ }
26
+
27
+ canHandle = ({ fromFile, targetHost }) => {
28
+ return fromFile?.type === typeMap.remote &&
29
+ fromFile?.host &&
30
+ targetHost &&
31
+ fromFile.host !== targetHost &&
32
+ fromFile?.tabId
33
+ }
34
+
35
+ createHandler = ({ fromFile, targetPathBase, targetTab }) => {
36
+ const handler = new Remote2RemoteHandler({
37
+ fromFile,
38
+ toPath: resolve(targetPathBase, fromFile.name),
39
+ sourceHost: fromFile.host,
40
+ sourceTabId: fromFile.tabId,
41
+ title: fromFile.title,
42
+ tabType: fromFile.tabType,
43
+ targetHost: targetTab.host,
44
+ targetTabId: targetTab.id,
45
+ targetTitle: targetTab.title || targetTab.host,
46
+ targetTabType: targetTab.type,
47
+ onDone: this.onDone
48
+ })
49
+ this.handlers.set(handler.id, handler)
50
+ handler.start()
51
+ }
52
+
53
+ onDone = ({ id, error }) => {
54
+ this.handlers.delete(id)
55
+ if (error) {
56
+ window.store.onError(new Error(error))
57
+ }
58
+ }
59
+
60
+ onRemote2RemoteDrop = ({ fromFiles, toFile, targetTab }) => {
61
+ const targetPathBase = resolve(toFile.path, toFile.name)
62
+ const targetHost = targetTab?.host
63
+ let handled = false
64
+ for (const fromFile of fromFiles) {
65
+ if (!this.canHandle({ fromFile, targetHost })) {
66
+ continue
67
+ }
68
+ handled = true
69
+ this.createHandler({
70
+ fromFile,
71
+ targetPathBase,
72
+ targetTab
73
+ })
74
+ }
75
+ return handled
76
+ }
77
+
78
+ render () {
79
+ return null
80
+ }
81
+ }
@@ -14,6 +14,7 @@ import Resolutions from '../rdp/resolution-edit'
14
14
  import TerminalInteractive from '../terminal/terminal-interactive'
15
15
  import ConfirmModalStore from '../file-transfer/conflict-resolve.jsx'
16
16
  import TransferQueue from '../file-transfer/transfer-queue'
17
+ import Remote2RemoteHandlers from '../file-transfer/remote2remote-handlers.jsx'
17
18
  import TerminalCmdSuggestions from '../terminal/terminal-command-dropdown'
18
19
  import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
20
  import classnames from 'classnames'
@@ -280,6 +281,7 @@ export default auto(function Index (props) {
280
281
  {...conflictStoreProps}
281
282
  config={config}
282
283
  />
284
+ <Remote2RemoteHandlers />
283
285
  <Resolutions {...resProps} />
284
286
  <InfoModal {...infoModalProps} />
285
287
  <RightSidePanel {...rightPanelProps}>
@@ -26,7 +26,7 @@ export default function ProfileFormSsh (props) {
26
26
  {...formItemLayout}
27
27
  label={e('password')}
28
28
  hasFeedback
29
- name={['rdp', 'password']}
29
+ name={['ftp', 'password']}
30
30
  >
31
31
  <Password />
32
32
  </FormItem>
@@ -584,6 +584,10 @@ export default class SettingCommon extends Component {
584
584
  {
585
585
  this.renderTextExec('execLinux')
586
586
  }
587
+ <div className='pd1b'>{e('keyword2FA')}</div>
588
+ {
589
+ this.renderText('keyword2FA')
590
+ }
587
591
  {
588
592
  [
589
593
  'autoRefreshWhenSwitchToSftp',
@@ -225,7 +225,21 @@ export default class FileSection extends React.Component {
225
225
  ? onDragCls + ' ' + onMultiDragCls
226
226
  : onDragCls
227
227
  addClass(this.domRef.current, cls)
228
- e.dataTransfer.setData('fromFile', JSON.stringify(this.props.file))
228
+ const transferProps = createTransferProps(this.props)
229
+ const selected = this.isSelected(this.props.file.id)
230
+ const dragFiles = selected
231
+ ? this.props.getSelectedFiles()
232
+ : [this.props.file]
233
+ const filesWithMeta = dragFiles.map(file => {
234
+ return {
235
+ ...file,
236
+ host: this.props.tab?.host,
237
+ tabType: this.props.tab?.type,
238
+ tabId: transferProps.tabId,
239
+ title: transferProps.title
240
+ }
241
+ })
242
+ e.dataTransfer.setData('fromFile', JSON.stringify(filesWithMeta))
229
243
  }
230
244
 
231
245
  getDropFileList = data => {
@@ -283,6 +297,23 @@ export default class FileSection extends React.Component {
283
297
  } = toFile
284
298
 
285
299
  let operation = ''
300
+ const targetHost = this.props.tab?.host
301
+ const isCrossHostRemoteDrop = !fromFileManager &&
302
+ fromType === typeMap.remote &&
303
+ toType === typeMap.remote &&
304
+ fromFiles.every(file => file?.host && file.host !== targetHost)
305
+
306
+ if (isCrossHostRemoteDrop) {
307
+ const handled = refsStatic.get('remote2remote-handlers')?.onRemote2RemoteDrop({
308
+ fromFiles,
309
+ toFile,
310
+ targetTab: this.props.tab
311
+ })
312
+ if (handled) {
313
+ return
314
+ }
315
+ }
316
+
286
317
  // same side and drop to file = drop to folder
287
318
  if (!fromFileManager && fromType === toType && !isDirectoryTo) {
288
319
  return
@@ -302,7 +302,7 @@ export default class Sftp extends Component {
302
302
  }, () => this[`${type}List`]())
303
303
  }
304
304
 
305
- updateCwd = (cwd) => {
305
+ updateCwd = (cwd = this.props.cwd) => {
306
306
  if (!this.state.inited) {
307
307
  return
308
308
  }
@@ -219,6 +219,27 @@ class ShortcutControl extends React.PureComponent {
219
219
  }
220
220
  }, 1000)
221
221
 
222
+ clickSftpIcon = () => {
223
+ const icon = document.querySelector('.session-current .sftp-follow-ssh-icon')
224
+ if (!icon) return
225
+ icon.click()
226
+ }
227
+
228
+ syncSftpPathShortcut = throttle((e) => {
229
+ e.stopPropagation()
230
+ const { activeTabId } = window.store
231
+ if (!activeTabId) return
232
+ // Get the SFTP component which can sync the path
233
+ const sftp = refs.get('sftp-' + activeTabId)
234
+ if (!sftp) return
235
+ this.clickSftpIcon()
236
+ // Wait for the path to be synced
237
+ setTimeout(() => {
238
+ this.clickSftpIcon()
239
+ sftp.updateCwd()
240
+ }, 100)
241
+ }, 1000)
242
+
222
243
  render () {
223
244
  return null
224
245
  }
@@ -115,6 +115,11 @@ export default () => {
115
115
  name: 'terminal_zoomoutTerminal',
116
116
  shortcut: 'ctrl+▼',
117
117
  shortcutMac: 'meta+▼'
118
+ },
119
+ {
120
+ name: 'terminal_syncSftpPath',
121
+ shortcut: 'alt+shift+f11',
122
+ shortcutMac: 'alt+shift+f11'
118
123
  }
119
124
  ]
120
125
  }
@@ -1097,6 +1097,7 @@ class Term extends Component {
1097
1097
  'execWindowsArgs',
1098
1098
  'execMacArgs',
1099
1099
  'execLinuxArgs',
1100
+ 'keyword2FA',
1100
1101
  'debug'
1101
1102
  ]),
1102
1103
  keepaliveInterval: tab.keepaliveInterval || config.keepaliveInterval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.11.16",
3
+ "version": "2.13.0",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",