@electerm/electerm-react 3.2.0 → 3.5.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.
Files changed (46) hide show
  1. package/client/common/constants.js +1 -8
  2. package/client/common/fs.js +84 -0
  3. package/client/components/batch-op/batch-op-alert.jsx +23 -0
  4. package/client/components/batch-op/batch-op-editor.jsx +206 -0
  5. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  6. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  7. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  8. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  9. package/client/components/bookmark-form/common/fields.jsx +15 -0
  10. package/client/components/bookmark-form/config/rdp.js +5 -0
  11. package/client/components/common/auto-check-update.jsx +31 -0
  12. package/client/components/common/notification.styl +1 -1
  13. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  14. package/client/components/footer/batch-input.jsx +10 -7
  15. package/client/components/main/error-wrapper.jsx +18 -7
  16. package/client/components/main/main.jsx +6 -7
  17. package/client/components/main/upgrade.jsx +133 -104
  18. package/client/components/main/upgrade.styl +2 -2
  19. package/client/components/rdp/file-transfer.js +375 -0
  20. package/client/components/rdp/rdp-session.jsx +169 -76
  21. package/client/components/rdp/rdp.styl +27 -0
  22. package/client/components/setting-sync/auto-sync.jsx +53 -0
  23. package/client/components/setting-sync/data-import.jsx +69 -8
  24. package/client/components/sftp/address-bar.jsx +23 -3
  25. package/client/components/sidebar/bookmark-select.jsx +3 -2
  26. package/client/components/sidebar/history-item.jsx +3 -1
  27. package/client/components/sidebar/index.jsx +0 -9
  28. package/client/components/sidebar/info-modal.jsx +7 -2
  29. package/client/components/tabs/add-btn-menu.jsx +1 -1
  30. package/client/components/tabs/add-btn.jsx +9 -15
  31. package/client/components/tabs/quick-connect.jsx +6 -10
  32. package/client/components/terminal/terminal.jsx +4 -5
  33. package/client/components/tree-list/tree-list.jsx +115 -10
  34. package/client/components/tree-list/tree-list.styl +3 -0
  35. package/client/components/tree-list/tree-search.jsx +9 -1
  36. package/client/components/widgets/widget-form.jsx +6 -0
  37. package/client/store/app-upgrade.js +2 -2
  38. package/client/store/common.js +0 -28
  39. package/client/store/load-data.js +3 -3
  40. package/client/store/mcp-handler.js +2 -2
  41. package/client/store/sync.js +25 -1
  42. package/client/store/tab.js +1 -1
  43. package/client/store/watch.js +10 -18
  44. package/client/views/index.pug +1 -2
  45. package/package.json +1 -1
  46. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -0,0 +1,315 @@
1
+ import { Component } from 'react'
2
+ import { refsStatic } from '../common/ref'
3
+ import { statusMap } from '../../common/constants'
4
+ import { autoRun } from 'manate'
5
+ import uid from '../../common/uid'
6
+
7
+ const STATIC_KEY = 'batch-op-runner'
8
+
9
+ export default class BatchOpRunner extends Component {
10
+ constructor () {
11
+ super()
12
+ this.steps = []
13
+ this.currentIndex = 0
14
+ this.status = 'idle'
15
+ this.currentTabId = null
16
+ this.currentStep = null
17
+ }
18
+
19
+ componentDidMount () {
20
+ refsStatic.add(STATIC_KEY, this)
21
+ }
22
+
23
+ getActiveTabId () {
24
+ return window.store?.activeTabId
25
+ }
26
+
27
+ getState () {
28
+ return {
29
+ steps: this.steps,
30
+ currentIndex: this.currentIndex,
31
+ status: this.status,
32
+ currentStep: this.currentStep
33
+ }
34
+ }
35
+
36
+ reset () {
37
+ this.steps = []
38
+ this.currentIndex = 0
39
+ this.status = 'idle'
40
+ this.currentTabId = null
41
+ this.currentStep = null
42
+ }
43
+
44
+ runBatchOpFromFile (filePath) {
45
+ return this._runBatchOpFromFile(filePath)
46
+ }
47
+
48
+ executeWorkflow (workflows) {
49
+ return this._executeWorkflow(workflows)
50
+ }
51
+
52
+ async _runBatchOpFromFile (filePath) {
53
+ try {
54
+ const content = await window.fs.readFile(filePath)
55
+ let workflows
56
+ try {
57
+ workflows = JSON.parse(content)
58
+ if (!Array.isArray(workflows)) {
59
+ throw new Error('Workflow must be an array')
60
+ }
61
+ } catch (e) {
62
+ console.error('Invalid batch operation JSON:', e.message)
63
+ return
64
+ }
65
+
66
+ this.reset()
67
+ await this._executeWorkflow(workflows)
68
+ console.log('Batch operation completed from file')
69
+ } catch (e) {
70
+ console.error('Failed to run batch operation from file:', e.message)
71
+ }
72
+ }
73
+
74
+ async _executeWorkflow (workflows) {
75
+ if (!Array.isArray(workflows)) {
76
+ throw new Error('Workflow must be an array')
77
+ }
78
+
79
+ this.steps = []
80
+ this.currentIndex = 0
81
+ this.status = 'running'
82
+
83
+ const results = []
84
+ const logsRef = refsStatic.get('batch-op-logs')
85
+
86
+ for (let i = 0; i < workflows.length; i++) {
87
+ const step = workflows[i]
88
+ this.currentIndex = i
89
+ this.currentStep = step.name
90
+
91
+ logsRef?.setLogs(this.getState())
92
+
93
+ let result
94
+ try {
95
+ result = await this._executeStep(step, results)
96
+ this.steps.push({ name: step.name, status: 'success', result })
97
+ results.push(result)
98
+ console.log(`Batch op step ${i + 1} completed:`, step.name || 'unnamed')
99
+ } catch (e) {
100
+ console.log(e)
101
+ this.steps.push({ name: step.name, status: 'error', error: e.message })
102
+ logsRef?.setLogs(this.getState())
103
+ console.error(`Batch op step ${i + 1} failed:`, step.name || 'unnamed', e.message)
104
+ this.status = 'error'
105
+ logsRef?.setLogs(this.getState())
106
+ throw e
107
+ }
108
+ }
109
+
110
+ this.status = 'completed'
111
+ this.currentStep = null
112
+ logsRef?.setLogs(this.getState())
113
+ }
114
+
115
+ async _executeStep (step, previousResults) {
116
+ const { action, prevDelay, afterDelay } = step
117
+
118
+ if (!action) {
119
+ throw new Error('Step must have an "action" field')
120
+ }
121
+
122
+ if (prevDelay > 0) {
123
+ await new Promise(resolve => setTimeout(resolve, prevDelay))
124
+ }
125
+
126
+ const s = step.params ? { ...step, ...step.params } : step
127
+
128
+ let result
129
+ switch (action) {
130
+ case 'connect':
131
+ result = await this._batchStepConnect(s)
132
+ this.currentTabId = result.tabId
133
+ break
134
+ case 'command':
135
+ result = await this._batchStepCommand(s)
136
+ break
137
+ case 'sftp_upload':
138
+ result = await this._batchStepSftpUpload(s)
139
+ break
140
+ case 'sftp_download':
141
+ result = await this._batchStepSftpDownload(s)
142
+ break
143
+ default:
144
+ throw new Error(`Unknown action: ${action}`)
145
+ }
146
+
147
+ if (afterDelay > 0) {
148
+ await new Promise(resolve => setTimeout(resolve, afterDelay))
149
+ }
150
+
151
+ return result
152
+ }
153
+
154
+ async _batchStepConnect (step) {
155
+ const { store } = window
156
+ const p = step.params || step
157
+
158
+ const tabId = uid()
159
+ const tab = {
160
+ id: tabId,
161
+ type: 'ssh',
162
+ host: p.host || '',
163
+ port: p.port || 22,
164
+ username: p.username || '',
165
+ password: p.password || '',
166
+ privateKey: p.privateKey || '',
167
+ passphrase: p.passphrase || '',
168
+ certificate: p.certificate || '',
169
+ authType: p.authType || 'password',
170
+ profile: p.profile || '',
171
+ enableSftp: p.enableSftp !== false,
172
+ enableSsh: p.enableSsh !== false,
173
+ useSshAgent: p.useSshAgent !== false,
174
+ sshAgent: p.sshAgent || '',
175
+ term: p.term || 'xterm-256color',
176
+ encode: p.encode || 'utf8',
177
+ envLang: p.envLang || 'en_US.UTF-8',
178
+ setEnv: p.setEnv || '',
179
+ startDirectoryRemote: p.startDirectoryRemote || '',
180
+ startDirectoryLocal: p.startDirectoryLocal || '',
181
+ proxy: p.proxy || '',
182
+ x11: p.x11 || false,
183
+ displayRaw: p.displayRaw || false,
184
+ sshTunnels: p.sshTunnels || [],
185
+ connectionHoppings: p.connectionHoppings || [],
186
+ title: step.name || `SSH: ${p.host}`,
187
+ status: 'processing',
188
+ pane: 'terminal'
189
+ }
190
+
191
+ store.addTab(tab)
192
+ await this._waitForConnection(tabId)
193
+
194
+ return {
195
+ success: true,
196
+ action: 'connect',
197
+ host: p.host,
198
+ port: p.port,
199
+ tabId
200
+ }
201
+ }
202
+
203
+ _waitForConnection = async (tabId) => {
204
+ return new Promise((resolve, reject) => {
205
+ const timeout = setTimeout(() => {
206
+ if (this._refWait) {
207
+ this._refWait.stop()
208
+ delete this._refWait
209
+ }
210
+ reject(new Error('Connection timeout'))
211
+ }, 30000)
212
+
213
+ this._refWait = autoRun(() => {
214
+ const { tabs } = window.store
215
+ const tab = tabs.find(t => t.id === tabId)
216
+ if (tab && tab.status === statusMap.success) {
217
+ clearTimeout(timeout)
218
+ this._refWait && this._refWait.stop()
219
+ delete this._refWait
220
+ resolve(tab)
221
+ } else if (tab && tab.status === statusMap.error) {
222
+ clearTimeout(timeout)
223
+ this._refWait && this._refWait.stop()
224
+ delete this._refWait
225
+ reject(new Error('Connection failed: ' + (tab.errorMsg || 'unknown error')))
226
+ }
227
+ return window.store.tabs
228
+ })
229
+ this._refWait.start()
230
+ })
231
+ }
232
+
233
+ async _batchStepCommand (step) {
234
+ const tabId = this.currentTabId
235
+
236
+ if (!tabId) {
237
+ throw new Error('No active tab. Please connect first.')
238
+ }
239
+
240
+ const { refs } = await import('../common/ref')
241
+ const term = refs.get('term-' + tabId)
242
+ if (!term || !term.term) {
243
+ throw new Error('Terminal not found')
244
+ }
245
+
246
+ let waited = 0
247
+ while (!term.attachAddon && waited < 10000) {
248
+ await new Promise(resolve => setTimeout(resolve, 200))
249
+ waited += 200
250
+ }
251
+ if (!term.attachAddon) {
252
+ throw new Error('Terminal not ready: attach addon not initialized')
253
+ }
254
+
255
+ term.runQuickCommand(step.command)
256
+
257
+ return {
258
+ success: true,
259
+ action: 'command',
260
+ command: step.command,
261
+ tabId
262
+ }
263
+ }
264
+
265
+ async _batchStepSftpUpload (step) {
266
+ const tabId = this.currentTabId
267
+ const { store } = window
268
+ const stepWithTabId = { ...step, tabId, conflictPolicy: 'mergeOrOverwriteAll' }
269
+ const { transferId } = await store.mcpSftpUpload(stepWithTabId)
270
+ await this._batchWaitForTransfer(transferId)
271
+ return { success: true, action: 'sftp_upload', localPath: step.localPath, remotePath: step.remotePath, transferId, tabId }
272
+ }
273
+
274
+ async _batchStepSftpDownload (step) {
275
+ const tabId = this.currentTabId
276
+ const { store } = window
277
+ const stepWithTabId = { ...step, tabId, conflictPolicy: 'mergeOrOverwriteAll' }
278
+ const { transferId } = await store.mcpSftpDownload(stepWithTabId)
279
+ await this._batchWaitForTransfer(transferId)
280
+ return { success: true, action: 'sftp_download', remotePath: step.remotePath, localPath: step.localPath, transferId, tabId }
281
+ }
282
+
283
+ async _batchWaitForTransfer (transferId) {
284
+ return new Promise((resolve, reject) => {
285
+ const timeout = setTimeout(() => {
286
+ if (this._refTransferWait) {
287
+ this._refTransferWait.stop()
288
+ delete this._refTransferWait
289
+ }
290
+ reject(new Error('Transfer timeout (1 hour)'))
291
+ }, 60 * 60 * 1000)
292
+
293
+ this._refTransferWait = autoRun(() => {
294
+ const { transferHistory } = window.store
295
+ const item = transferHistory.find(t => t.id === transferId || t.originalId === transferId)
296
+ if (item) {
297
+ clearTimeout(timeout)
298
+ this._refTransferWait && this._refTransferWait.stop()
299
+ delete this._refTransferWait
300
+ if (item.error) {
301
+ reject(new Error('Transfer failed: ' + item.error))
302
+ } else {
303
+ resolve(item)
304
+ }
305
+ }
306
+ return window.store.transferHistory
307
+ })
308
+ this._refTransferWait.start()
309
+ })
310
+ }
311
+
312
+ render () {
313
+ return null
314
+ }
315
+ }
@@ -2,7 +2,8 @@
2
2
  * AI-powered bookmark generation form
3
3
  */
4
4
  import { useState, useEffect } from 'react'
5
- import { Button, Input, message, Space, Alert } from 'antd'
5
+ import { Button, Input, Space, Alert } from 'antd'
6
+ import message from '../common/message'
6
7
  import {
7
8
  RobotOutlined,
8
9
  LoadingOutlined,
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import React from 'react'
6
- import { Button, message } from 'antd'
6
+ import { Button } from 'antd'
7
+ import message from '../common/message'
7
8
  import { PlusOutlined } from '@ant-design/icons'
8
9
  import Modal from '../common/modal'
9
10
  import { refsStatic } from '../common/ref'
@@ -22,6 +22,7 @@ import SshHostSelector from './ssh-host-selector.jsx'
22
22
  import SshAuthTypeSelector from './ssh-auth-type-selector.jsx'
23
23
  import SshAuthSelector from './ssh-auth-selector.jsx'
24
24
  import CategorySelect from './category-select.jsx'
25
+ import ExternalLink from '../../common/external-link.jsx'
25
26
  const Fragment = React.Fragment
26
27
  const FormItem = Form.Item
27
28
 
@@ -115,6 +116,20 @@ export function renderFormItem (item, formItemLayout, form, ctxProps, index) {
115
116
 
116
117
  // Render complex/custom components directly (no extra wrapper component)
117
118
  switch (type) {
119
+ case 'wiki':
120
+ return (
121
+ <Alert
122
+ key={name}
123
+ type='warning'
124
+ className='mg2b'
125
+ showIcon
126
+ description={
127
+ <>
128
+ <ExternalLink to={item.link}>{item.link}</ExternalLink>
129
+ </>
130
+ }
131
+ />
132
+ )
118
133
  case 'alert':
119
134
  return <Alert key={name} {...item.props} />
120
135
  case 'info':
@@ -21,6 +21,11 @@ const rdpConfig = {
21
21
  key: 'auth',
22
22
  label: e('auth'),
23
23
  fields: [
24
+ {
25
+ type: 'wiki',
26
+ name: 'rdp-limitation-warning',
27
+ link: 'https://github.com/electerm/electerm/wiki/RDP-limitation'
28
+ },
24
29
  commonFields.category,
25
30
  commonFields.colorTitle,
26
31
  { type: 'input', name: 'host', label: () => e('host'), rules: [{ required: true, message: e('host') + ' required' }] },
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ export default function AutoCheckUpdate ({ config }) {
4
+ const lastCheckTimeRef = useRef(0)
5
+ const intervalIdRef = useRef(null)
6
+
7
+ useEffect(() => {
8
+ if (!config.checkUpdateOnStart) {
9
+ clearInterval(intervalIdRef.current)
10
+ return
11
+ }
12
+
13
+ const checkForUpdate = () => {
14
+ const { store } = window
15
+ if (store.config.checkUpdateOnStart) {
16
+ store.onCheckUpdate(false)
17
+ }
18
+ lastCheckTimeRef.current = Date.now()
19
+ }
20
+
21
+ intervalIdRef.current = setInterval(checkForUpdate, 60 * 60 * 1000)
22
+
23
+ return () => {
24
+ if (intervalIdRef.current) {
25
+ clearInterval(intervalIdRef.current)
26
+ }
27
+ }
28
+ }, [config.checkUpdateOnStart])
29
+
30
+ return null
31
+ }
@@ -2,7 +2,7 @@
2
2
  position fixed
3
3
  bottom 20px
4
4
  right 20px
5
- z-index 1000
5
+ z-index 9999
6
6
 
7
7
  .notification
8
8
  background var(--main-lighter)
@@ -21,6 +21,9 @@ function formatTimeAuto (strOrDigit) {
21
21
  if (isString(strOrDigit)) {
22
22
  return formatTime(strOrDigit)
23
23
  }
24
+ if (strOrDigit > 9999999999) {
25
+ return formatTime(strOrDigit)
26
+ }
24
27
  return formatTime(strOrDigit * 1000)
25
28
  }
26
29
 
@@ -136,12 +136,10 @@ export default class BatchInput extends Component {
136
136
  } = this.props
137
137
  const opts = {
138
138
  options: this.buildOptions(),
139
- placeholder: e('batchInput'),
140
139
  value: cmd,
141
140
  onChange: this.handleChange,
142
141
  defaultOpen: false,
143
142
  open,
144
- allowClear: true,
145
143
  className: 'batch-input-wrap'
146
144
  }
147
145
  const cls = classNames(
@@ -155,6 +153,15 @@ export default class BatchInput extends Component {
155
153
  placeholder: e('batchInput'),
156
154
  className: 'batch-input-holder'
157
155
  }
156
+ const textAreaProps = {
157
+ onPressEnter: this.handleEnter,
158
+ onClick: this.handleClick,
159
+ onBlur: this.handleBlur,
160
+ size: 'small',
161
+ autoSize: { minRows: 1 },
162
+ placeholder: e('batchInput'),
163
+ allowClear: true
164
+ }
158
165
  const tabSelectProps = {
159
166
  activeTabId: this.props.activeTabId,
160
167
  tabs: this.getTabs(),
@@ -179,11 +186,7 @@ export default class BatchInput extends Component {
179
186
  {...opts}
180
187
  >
181
188
  <Input.TextArea
182
- onPressEnter={this.handleEnter}
183
- onClick={this.handleClick}
184
- onBlur={this.handleBlur}
185
- size='small'
186
- autoSize={{ minRows: 1 }}
189
+ {...textAreaProps}
187
190
  />
188
191
  </AutoComplete>
189
192
  <TabSelect {...tabSelectProps} />
@@ -77,12 +77,9 @@ export default class ErrorBoundary extends React.PureComponent {
77
77
  }
78
78
 
79
79
  renderTroubleShoot = () => {
80
- const {
81
- bugs: {
82
- url: bugReportLink
83
- }
84
- } = packInfo
85
- const bugUrl = `${bugReportLink}/new/choose`
80
+ if (window.et.isWebApp) {
81
+ return this.renderContacts()
82
+ }
86
83
  return (
87
84
  <div className='pd1y wordbreak'>
88
85
  <h2>{e('troubleShoot')}</h2>
@@ -99,6 +96,20 @@ export default class ErrorBoundary extends React.PureComponent {
99
96
  )
100
97
  })
101
98
  }
99
+ {this.renderContacts()}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ renderContacts () {
105
+ const {
106
+ bugs: {
107
+ url: bugReportLink
108
+ }
109
+ } = packInfo
110
+ const bugUrl = `${bugReportLink}/new/choose`
111
+ return (
112
+ <>
102
113
  <div className='pd1b'>
103
114
  <Link to={bugUrl}>{e('bugReport')}</Link>
104
115
  </div>
@@ -112,7 +123,7 @@ export default class ErrorBoundary extends React.PureComponent {
112
123
  className='mwm-100'
113
124
  />
114
125
  </div>
115
- </div>
126
+ </>
116
127
  )
117
128
  }
118
129
 
@@ -6,7 +6,6 @@ import UpdateCheck from './upgrade'
6
6
  import SettingModal from '../setting-panel/setting-modal'
7
7
  import TextEditor from '../text-editor/text-editor'
8
8
  import Sidebar from '../sidebar'
9
- import BatchOp from '../batch-op/batch-op'
10
9
  import CssOverwrite from '../bg/css-overwrite'
11
10
  import UiTheme from './ui-theme'
12
11
  import CustomCss from '../bg/custom-css.jsx'
@@ -34,6 +33,9 @@ import MoveItemModal from '../tree-list/move-item-modal'
34
33
  import InputContextMenu from '../common/input-context-menu'
35
34
  import WorkspaceSaveModal from '../tabs/workspace-save-modal'
36
35
  import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
36
+ import AutoSync from '../setting-sync/auto-sync'
37
+ import AutoCheckUpdate from '../common/auto-check-update'
38
+ import BatchOpRunner from '../batch-op/batch-op-runner'
37
39
  import { pick } from 'lodash-es'
38
40
  import deepCopy from 'json-deep-copy'
39
41
  import './wrapper.styl'
@@ -184,11 +186,6 @@ export default auto(function Index (props) {
184
186
  fileTransferChanged: JSON.stringify(copiedTransfer),
185
187
  fileTransfers: copiedTransfer
186
188
  }
187
- const batchOpProps = {
188
- transferHistory,
189
- showModal: store.showModal,
190
- innerWidth: store.innerWidth
191
- }
192
189
  const resProps = {
193
190
  resolutions: deepCopy(store.resolutions),
194
191
  openResolutionEdit
@@ -263,7 +260,6 @@ export default auto(function Index (props) {
263
260
  />
264
261
  <FileInfoModal />
265
262
  <SettingModal store={store} />
266
- <BatchOp {...batchOpProps} />
267
263
  <MoveItemModal store={store} />
268
264
  <div
269
265
  id='outside-context'
@@ -295,9 +291,12 @@ export default auto(function Index (props) {
295
291
  <ConnectionHoppingWarning {...warningProps} />
296
292
  <TerminalCmdSuggestions {...cmdSuggestionsProps} />
297
293
  <TransferQueue />
294
+ <AutoSync config={config} />
295
+ <AutoCheckUpdate config={config} />
298
296
  <WorkspaceSaveModal store={store} />
299
297
  <BookmarkFromHistoryModal />
300
298
  <NotificationContainer />
299
+ <BatchOpRunner />
301
300
  </div>
302
301
  </ConfigProvider>
303
302
  )