@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
@@ -229,11 +229,6 @@ export const rendererTypes = {
229
229
  canvas: 'canvas',
230
230
  webGL: 'webGL'
231
231
  }
232
- export const mirrors = {
233
- 'download-electerm': 'download-electerm',
234
- github: 'github',
235
- sourceforge: 'sourceforge'
236
- }
237
232
  export const downloadUpgradeTimeout = 20000
238
233
  export const expandedKeysLsKey = 'expanded-keys'
239
234
  export const resolutionsLsKey = 'custom-resolution-key'
@@ -242,15 +237,13 @@ export const quickCommandLabelsLsKey = 'quick-command-label'
242
237
  export const localAddrBookmarkLsKey = 'local-addr-bookmark-keys'
243
238
  export const dismissDelKeyTipLsKey = 'dismiss-del-key-tip'
244
239
  export const sshTunnelHelpLink = 'https://github.com/electerm/electerm/wiki/How-to-use-ssh-tunnel'
245
- export const batchOpHelpLink = 'https://github.com/electerm/electerm/wiki/batch-operation'
246
240
  export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-format'
247
241
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
248
242
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
249
243
  export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
250
244
  export const modals = {
251
245
  hide: 0,
252
- setting: 1,
253
- batchOps: 2
246
+ setting: 1
254
247
  }
255
248
  export const instSftpKeys = [
256
249
  'connect',
@@ -16,6 +16,90 @@ const fs = fsFunctions.reduce((prev, func) => {
16
16
  return prev
17
17
  }, {})
18
18
 
19
+ // Encoding function
20
+ fs.encodeUint8Array = (uint8Array) => {
21
+ let str = ''
22
+ const len = uint8Array.byteLength
23
+
24
+ for (let i = 0; i < len; i++) {
25
+ str += String.fromCharCode(uint8Array[i])
26
+ }
27
+
28
+ return btoa(str)
29
+ }
30
+
31
+ // Decoding function
32
+ fs.decodeBase64String = (base64String) => {
33
+ const str = atob(base64String)
34
+ const len = str.length
35
+
36
+ const uint8Array = new Uint8Array(len)
37
+
38
+ for (let i = 0; i < len; i++) {
39
+ uint8Array[i] = str.charCodeAt(i)
40
+ }
41
+
42
+ return uint8Array
43
+ }
44
+
45
+ Object.assign(fs, {
46
+ stat: (path, cb) => {
47
+ window.fs.statCustom(path)
48
+ .catch(err => cb(err))
49
+ .then(obj => {
50
+ obj.isDirectory = () => obj.isD
51
+ obj.isFile = () => obj.isF
52
+ cb(undefined, obj)
53
+ })
54
+ },
55
+ access: (...args) => {
56
+ const cb = args.pop()
57
+ window.fs.access(...args)
58
+ .then((data) => cb(undefined, data))
59
+ .catch((err) => cb(err))
60
+ },
61
+ open: (...args) => {
62
+ const cb = args.pop()
63
+ window.fs.openCustom(...args)
64
+ .then((data) => cb(undefined, data))
65
+ .catch((err) => cb(err))
66
+ },
67
+ read: (p1, arr, ...args) => {
68
+ const cb = args.pop()
69
+ window.fs.readCustom(
70
+ p1,
71
+ arr.length,
72
+ ...args
73
+ )
74
+ .then((data) => {
75
+ const { n, newArr } = data
76
+ const newArr1 = window.fs.decodeBase64String(newArr)
77
+ cb(undefined, n, newArr1)
78
+ })
79
+ .catch(err => cb(err))
80
+ },
81
+ close: (fd, cb) => {
82
+ window.fs.closeCustom(fd)
83
+ .then((data) => cb(undefined, data))
84
+ .catch((err) => cb(err))
85
+ },
86
+ readdir: (p, cb) => {
87
+ window.fs.readdir(p)
88
+ .then((data) => cb(undefined, data))
89
+ .catch((err) => cb(err))
90
+ },
91
+ write: (p1, buf, cb) => {
92
+ window.fs.writeCustom(p1, window.fs.encodeUint8Array(buf))
93
+ .then((data) => cb(undefined, data))
94
+ .catch((err) => cb(err))
95
+ },
96
+ realpath: (p, cb) => {
97
+ window.fs.realpath(p)
98
+ .then((data) => cb(undefined, data))
99
+ .catch((err) => cb(err))
100
+ }
101
+ })
102
+
19
103
  window.fs = fs
20
104
 
21
105
  export default fs
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import { Alert } from 'antd'
3
+ import ExternalLink from '../common/external-link'
4
+
5
+ const batchOpWikiLink = 'https://github.com/electerm/electerm/wiki/batch-operation'
6
+
7
+ export default function BatchOpAlert () {
8
+ const description = (
9
+ <>
10
+ <p>Actions: <code>connect, command, sftp_upload, sftp_download</code></p>
11
+ <div><ExternalLink to={batchOpWikiLink}>{batchOpWikiLink}</ExternalLink></div>
12
+ </>
13
+ )
14
+
15
+ return (
16
+ <Alert
17
+ description={description}
18
+ type='info'
19
+ showIcon
20
+ className='mg1b'
21
+ />
22
+ )
23
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Batch Operation Editor Component
3
+ * Self-contained workflow editor: handles execute, external editors, and progress logs
4
+ */
5
+ import React, { useCallback, useState, useEffect } from 'react'
6
+ import { Button, Flex } from 'antd'
7
+ import {
8
+ PlayCircleOutlined
9
+ } from '@ant-design/icons'
10
+ import SimpleEditor from '../text-editor/simple-editor'
11
+ import EditWithCustomEditor from '../text-editor/edit-with-custom-editor'
12
+ import BatchOpAlert from './batch-op-alert'
13
+ import BatchOpLogs from './batch-op-logs'
14
+ import message from '../common/message'
15
+ import { refsStatic } from '../common/ref'
16
+ import generate from '../../common/uid'
17
+ import fs from '../../common/fs'
18
+ import { safeGetItem, safeSetItem } from '../../common/safe-local-storage'
19
+
20
+ const batchOpEditorKey = 'batch-op-editor-content'
21
+ const workflowExample = `[
22
+ {
23
+ "name": "Connect SSH",
24
+ "action": "connect",
25
+ "params": {
26
+ "host": "192.168.1.100",
27
+ "port": 22,
28
+ "username": "root",
29
+ "authType": "password",
30
+ "password": "your_password"
31
+ }
32
+ },
33
+ {
34
+ "name": "Create 5M Test File",
35
+ "action": "command",
36
+ "afterDelay": 500,
37
+ "prevDelay": 500,
38
+ "command": "fallocate -l 5M /tmp/test_5m_file.bin && rm -f /tmp/test_log.log && echo '[LOG] Created 5M test file at $(date)' >> /tmp/test_log.log"
39
+ },
40
+ {
41
+ "name": "Log creation",
42
+ "action": "command",
43
+ "command": "ls -la /tmp/test_5m_file.bin >> /tmp/test_log.log 2>&1 && echo '[LOG] File size logged at $(date)' >> /tmp/test_log.log"
44
+ },
45
+ {
46
+ "name": "Download 5M File",
47
+ "action": "sftp_download",
48
+ "afterDelay": 200,
49
+ "remotePath": "/tmp/test_5m_file.bin",
50
+ "localPath": "/tmp/test_5m_file.bin"
51
+ },
52
+ {
53
+ "name": "Log after download",
54
+ "action": "command",
55
+ "afterDelay": 200,
56
+ "command": "echo '[LOG] Download complete at $(date)' >> /tmp/test_log.log"
57
+ },
58
+ {
59
+ "name": "Delete Remote 5M File",
60
+ "action": "command",
61
+ "afterDelay": 200,
62
+ "command": "rm /tmp/test_5m_file.bin && echo '[LOG] Deleted remote 5M file at $(date)' >> /tmp/test_log.log"
63
+ },
64
+ {
65
+ "name": "Upload Downloaded File to Remote",
66
+ "action": "sftp_upload",
67
+ "afterDelay": 200,
68
+ "localPath": "/tmp/test_5m_file.bin",
69
+ "remotePath": "/tmp/test_5m_file_uploaded.bin"
70
+ },
71
+ {
72
+ "name": "Log after upload",
73
+ "action": "command",
74
+ "afterDelay": 200,
75
+ "command": "echo '[LOG] Upload complete at $(date)' >> /tmp/test_log.log"
76
+ },
77
+ {
78
+ "name": "Verify and clean up",
79
+ "action": "command",
80
+ "command": "ls -la /tmp/test_5m_file_uploaded.bin >> /tmp/test_log.log 2>&1 && rm -f /tmp/test_5m_file*.bin && echo '[LOG] Cleaned up at $(date)' >> /tmp/test_log.log"
81
+ }
82
+ ]`
83
+
84
+ function getDefaultValue (widget) {
85
+ const saved = safeGetItem(batchOpEditorKey)
86
+ if (saved) return saved
87
+ return workflowExample
88
+ }
89
+
90
+ export default function BatchOpEditor ({ widget }) {
91
+ const [value, setValue] = useState(() => getDefaultValue(widget))
92
+ const [executing, setExecuting] = useState(false)
93
+
94
+ useEffect(() => {
95
+ const v = getDefaultValue(widget)
96
+ if (v) setValue(v)
97
+ }, [widget?.id])
98
+
99
+ useEffect(() => {
100
+ safeSetItem(batchOpEditorKey, value)
101
+ }, [value])
102
+
103
+ const handleExecute = async () => {
104
+ if (!value || executing) return
105
+ setExecuting(true)
106
+ const runner = refsStatic.get('batch-op-runner')
107
+ runner?.reset()
108
+ refsStatic.get('batch-op-logs')?.setLogs({ steps: [], currentIndex: 0, status: 'running' })
109
+ try {
110
+ let workflows
111
+ try {
112
+ workflows = JSON.parse(value)
113
+ if (!Array.isArray(workflows)) throw new Error('Workflow must be an array')
114
+ } catch (e) {
115
+ message.error('Invalid workflow JSON: ' + e.message)
116
+ refsStatic.get('batch-op-logs')?.reset()
117
+ return
118
+ }
119
+ await runner.executeWorkflow(workflows)
120
+ message.success('Workflow execution completed')
121
+ } catch (err) {
122
+ if (err.message !== 'Workflow aborted') {
123
+ message.error('Workflow execution failed: ' + err.message)
124
+ }
125
+ } finally {
126
+ setExecuting(false)
127
+ }
128
+ }
129
+
130
+ const handleTemplate = useCallback(() => {
131
+ setValue(workflowExample)
132
+ }, [])
133
+
134
+ const handleEditWithSystemEditor = useCallback(async () => {
135
+ const id = generate()
136
+ const tempPath = window.pre.resolve(window.pre.tempDir, `electerm-batch-op-${id}.json`)
137
+ await fs.writeFile(tempPath, value)
138
+ window.pre.runGlobalAsync('watchFile', tempPath)
139
+ fs.openFile(tempPath).catch(window.store.onError)
140
+ window.pre.showItemInFolder(tempPath)
141
+ const onFileChange = (e, text) => {
142
+ setValue(text)
143
+ window.pre.ipcOffEvent('file-change', onFileChange)
144
+ fs.unlink(tempPath).catch(console.log)
145
+ }
146
+ window.pre.ipcOnEvent('file-change', onFileChange)
147
+ }, [value])
148
+
149
+ const handleEditWithCustom = useCallback(async (editorCommand) => {
150
+ const id = generate()
151
+ const tempPath = window.pre.resolve(window.pre.tempDir, `electerm-batch-op-${id}.json`)
152
+ await fs.writeFile(tempPath, value)
153
+ window.pre.runGlobalAsync('watchFile', tempPath)
154
+ await window.pre.runGlobalAsync('openFileWithEditor', tempPath, editorCommand)
155
+ const onFileChange = (e, text) => {
156
+ setValue(text)
157
+ window.pre.ipcOffEvent('file-change', onFileChange)
158
+ fs.unlink(tempPath).catch(console.log)
159
+ }
160
+ window.pre.ipcOnEvent('file-change', onFileChange)
161
+ }, [value])
162
+
163
+ function handleChange (e) {
164
+ setValue(e.target.value)
165
+ }
166
+
167
+ return (
168
+ <div className='batch-op-editor'>
169
+ <BatchOpAlert />
170
+ <Flex className='mg2y' gap='small'>
171
+ <Button onClick={handleTemplate} type='dashed'>
172
+ Load Template
173
+ </Button>
174
+ <Button
175
+ onClick={handleExecute}
176
+ type='primary'
177
+ loading={executing}
178
+ disabled={executing}
179
+ icon={<PlayCircleOutlined />}
180
+ >
181
+ Execute Workflow
182
+ </Button>
183
+ </Flex>
184
+ <SimpleEditor
185
+ value={value}
186
+ onChange={handleChange}
187
+ />
188
+ {!window.et.isWebApp && (
189
+ <div className='pd1t pd2b'>
190
+ <Button
191
+ type='primary'
192
+ className='mg1r mg1b'
193
+ onClick={handleEditWithSystemEditor}
194
+ >
195
+ {window.translate('editWithSystemEditor')}
196
+ </Button>
197
+ <EditWithCustomEditor
198
+ loading={executing}
199
+ editWithCustom={handleEditWithCustom}
200
+ />
201
+ </div>
202
+ )}
203
+ <BatchOpLogs />
204
+ </div>
205
+ )
206
+ }
@@ -0,0 +1,53 @@
1
+ import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react'
2
+ import { refsStatic } from '../common/ref'
3
+
4
+ const STATIC_KEY = 'batch-op-logs'
5
+
6
+ const BatchOpLogs = forwardRef(function BatchOpLogs (_, ref) {
7
+ const [logs, setLogsState] = useState(null)
8
+
9
+ useImperativeHandle(ref, () => ({
10
+ setLogs: (progress) => setLogsState(progress),
11
+ reset: () => setLogsState(null)
12
+ }))
13
+
14
+ useEffect(() => {
15
+ refsStatic.add(STATIC_KEY, {
16
+ setLogs: (progress) => setLogsState(progress),
17
+ reset: () => setLogsState(null)
18
+ })
19
+ return () => refsStatic.remove(STATIC_KEY)
20
+ }, [])
21
+
22
+ if (!logs || (!logs.steps.length && !logs.currentStep)) {
23
+ return null
24
+ }
25
+
26
+ const statusIcon = { success: '✓', error: '✗' }
27
+
28
+ return (
29
+ <div className='batch-op-logs mg1t pd1 font13'>
30
+ <div className='bold mg1b'>Execution Log</div>
31
+ {logs.steps.map((step, i) => (
32
+ <div key={i} className={`batch-op-log-entry ${step.status}`}>
33
+ <span className='log-icon mg1r'>{statusIcon[step.status] || '○'}</span>
34
+ <span className='log-name'>{step.name}</span>
35
+ {step.error && <span className='log-error mg1l color-red'>{step.error}</span>}
36
+ </div>
37
+ ))}
38
+ {logs.status === 'running' && logs.currentStep && (
39
+ <div className='batch-op-log-entry running'>
40
+ <span className='log-icon mg1r'>→</span>
41
+ <span className='log-name'>{logs.currentStep}</span>
42
+ </div>
43
+ )}
44
+ {logs.status === 'completed' && (
45
+ <div className='batch-op-log-entry completed color-green mg1t'>
46
+ ✓ Workflow completed
47
+ </div>
48
+ )}
49
+ </div>
50
+ )
51
+ })
52
+
53
+ export default BatchOpLogs