@electerm/electerm-react 3.11.11 → 3.12.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.
@@ -38,6 +38,21 @@ export default auto(function HistoryPanel (props) {
38
38
  store.clearHistory()
39
39
  }
40
40
  const e = window.translate
41
+ function renderHeader () {
42
+ if (!arr.length) {
43
+ return null
44
+ }
45
+ return (
46
+ <div className='history-header pd2x pd2b'>
47
+ <Switch
48
+ {...switchProps}
49
+ />
50
+ <UnorderedListOutlined
51
+ {...clearIconProps}
52
+ />
53
+ </div>
54
+ )
55
+ }
41
56
  const switchProps = {
42
57
  checkedChildren: e('sortByFrequency'),
43
58
  unCheckedChildren: e('sortByFrequency'),
@@ -54,14 +69,7 @@ export default auto(function HistoryPanel (props) {
54
69
  <div
55
70
  className='sidebar-panel-history'
56
71
  >
57
- <div className='history-header pd2x pd2b'>
58
- <Switch
59
- {...switchProps}
60
- />
61
- <UnorderedListOutlined
62
- {...clearIconProps}
63
- />
64
- </div>
72
+ {renderHeader()}
65
73
  <div className='history-body'>
66
74
  {
67
75
  arr.map((item, i) => {
@@ -96,7 +96,7 @@ export default class AttachAddonCustom {
96
96
  if (typeof ev.data === 'string') {
97
97
  try {
98
98
  const msg = JSON.parse(ev.data)
99
- if (msg.action === 'zmodem-event' || msg.action === 'trzsz-event') {
99
+ if (msg.action === 'zmodem-event' || msg.action === 'trzsz-event' || msg.action === 'xmodem-event') {
100
100
  return
101
101
  }
102
102
  } catch (e) {}
@@ -4,11 +4,62 @@ import Modal from '../common/modal'
4
4
  const e = window.translate
5
5
 
6
6
  export class DropFileModal extends Component {
7
+ renderSerialFooter () {
8
+ const { onSelect } = this.props
9
+ return (
10
+ <>
11
+ <button
12
+ type='button'
13
+ className='custom-modal-ok-btn'
14
+ onClick={() => onSelect('xmodem')}
15
+ >
16
+ XMODEM
17
+ </button>
18
+ <button
19
+ type='button'
20
+ className='custom-modal-cancel-btn'
21
+ onClick={() => onSelect('inputOnly')}
22
+ >
23
+ {e('inputOnly')}
24
+ </button>
25
+ </>
26
+ )
27
+ }
28
+
29
+ renderSshFooter () {
30
+ const { onSelect } = this.props
31
+ return (
32
+ <>
33
+ <button
34
+ type='button'
35
+ className='custom-modal-ok-btn'
36
+ onClick={() => onSelect('trz')}
37
+ >
38
+ trz
39
+ </button>
40
+ <button
41
+ type='button'
42
+ className='custom-modal-cancel-btn'
43
+ onClick={() => onSelect('rz')}
44
+ >
45
+ rz
46
+ </button>
47
+ <button
48
+ type='button'
49
+ className='custom-modal-cancel-btn'
50
+ onClick={() => onSelect('inputOnly')}
51
+ >
52
+ {e('inputOnly')}
53
+ </button>
54
+ </>
55
+ )
56
+ }
57
+
7
58
  render () {
8
59
  const {
9
60
  visible,
10
61
  files,
11
- onSelect,
62
+ isSerial,
12
63
  onCancel
13
64
  } = this.props
14
65
 
@@ -23,27 +74,7 @@ export class DropFileModal extends Component {
23
74
  onCancel={onCancel}
24
75
  footer={
25
76
  <div className='custom-modal-footer-buttons'>
26
- <button
27
- type='button'
28
- className='custom-modal-ok-btn'
29
- onClick={() => onSelect('trz')}
30
- >
31
- trz
32
- </button>
33
- <button
34
- type='button'
35
- className='custom-modal-cancel-btn'
36
- onClick={() => onSelect('rz')}
37
- >
38
- rz
39
- </button>
40
- <button
41
- type='button'
42
- className='custom-modal-cancel-btn'
43
- onClick={() => onSelect('inputOnly')}
44
- >
45
- {e('inputOnly')}
46
- </button>
77
+ {isSerial ? this.renderSerialFooter() : this.renderSshFooter()}
47
78
  </div>
48
79
  }
49
80
  width={400}
@@ -26,6 +26,7 @@ import AttachAddon from './attach-addon-custom.js'
26
26
  import getProxy from '../../common/get-proxy.js'
27
27
  import { ZmodemClient } from './zmodem-client.js'
28
28
  import { TrzszClient } from './trzsz-client.js'
29
+ import { XmodemClient } from './xmodem-client.js'
29
30
  import DropFileModal from './drop-file-modal.jsx'
30
31
  import keyControlPressed from '../../common/key-control-pressed.js'
31
32
  import NormalBuffer from './normal-buffer.jsx'
@@ -165,6 +166,7 @@ class Term extends Component {
165
166
  this.fitAddon = null
166
167
  this.zmodemClient = null
167
168
  this.trzszClient = null
169
+ this.xmodemClient = null
168
170
  this.searchAddon = null
169
171
  this.fitAddon = null
170
172
  this.cmdAddon = null
@@ -391,6 +393,7 @@ class Term extends Component {
391
393
  const fromFile = dt.getData('fromFile')
392
394
  const notSafeMsg = 'File name contains unsafe characters'
393
395
  const isSshTerminal = this.props.tab.type === connectionMap.ssh
396
+ const isSerialTerminal = this.props.tab.type === connectionMap.serial
394
397
 
395
398
  if (fromFile) {
396
399
  try {
@@ -412,6 +415,13 @@ class Term extends Component {
412
415
  }
413
416
  return
414
417
  }
418
+ if (isSerialTerminal) {
419
+ this.setState({
420
+ dropFileModalVisible: true,
421
+ droppedFiles: [{ path: filePath, isRemote: false }]
422
+ })
423
+ return
424
+ }
415
425
  this.attachAddon._sendData(`"${filePath}" `)
416
426
  return
417
427
  } catch (e) {
@@ -443,6 +453,14 @@ class Term extends Component {
443
453
  return
444
454
  }
445
455
 
456
+ if (isSerialTerminal) {
457
+ this.setState({
458
+ dropFileModalVisible: true,
459
+ droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
460
+ })
461
+ return
462
+ }
463
+
446
464
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
447
465
  this.attachAddon._sendData(filesAll)
448
466
  }
@@ -485,6 +503,19 @@ class Term extends Component {
485
503
  this.attachAddon._sendData('rz\r')
486
504
  break
487
505
  }
506
+ case 'xmodem': {
507
+ if (this.xmodemClient && this.xmodemClient.isActive) {
508
+ message.warning('A transfer is already in progress')
509
+ this.handleDropFileModalCancel()
510
+ return
511
+ }
512
+ // Use XMODEM send with the dropped files
513
+ window._apiControlSelectFile = filePaths
514
+ if (this.xmodemClient) {
515
+ this.xmodemClient.initiateSend()
516
+ }
517
+ break
518
+ }
488
519
  case 'inputOnly':
489
520
  default: {
490
521
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
@@ -653,7 +684,8 @@ class Term extends Component {
653
684
  const clearShortcut = this.getShortcut('terminal_clear')
654
685
  const searchShortcut = this.getShortcut('terminal_search')
655
686
  const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
656
- return [
687
+ const isSerial = this.props.tab?.type === connectionMap.serial
688
+ const items = [
657
689
  {
658
690
  key: 'onCopy',
659
691
  icon: <iconsMap.CopyOutlined />,
@@ -700,12 +732,44 @@ class Term extends Component {
700
732
  extra: searchShortcut
701
733
  }
702
734
  ]
735
+ if (isSerial) {
736
+ items.push(
737
+ {
738
+ type: 'divider'
739
+ },
740
+ {
741
+ key: 'onXmodemSend',
742
+ icon: <iconsMap.CloudUploadOutlined />,
743
+ label: 'XMODEM Send'
744
+ },
745
+ {
746
+ key: 'onXmodemReceive',
747
+ icon: <iconsMap.CloudDownloadOutlined />,
748
+ label: 'XMODEM Receive'
749
+ }
750
+ )
751
+ }
752
+ return items
703
753
  }
704
754
 
705
755
  onContextMenu = ({ key }) => {
706
756
  this[key]()
707
757
  }
708
758
 
759
+ onXmodemSend = () => {
760
+ if (this.xmodemClient) {
761
+ this.xmodemClient.initiateSend()
762
+ }
763
+ this.term.focus()
764
+ }
765
+
766
+ onXmodemReceive = () => {
767
+ if (this.xmodemClient) {
768
+ this.xmodemClient.initiateReceive()
769
+ }
770
+ this.term.focus()
771
+ }
772
+
709
773
  notifyOnData = debounce(() => {
710
774
  window.store.notifyTabOnData(this.props.tab.id)
711
775
  }, 1000)
@@ -1320,6 +1384,8 @@ class Term extends Component {
1320
1384
  this.zmodemClient.init(socket)
1321
1385
  this.trzszClient = new TrzszClient(this)
1322
1386
  this.trzszClient.init(socket)
1387
+ this.xmodemClient = new XmodemClient(this)
1388
+ this.xmodemClient.init(socket)
1323
1389
  this.fitAddon.fit()
1324
1390
  term.displayRaw = displayRaw
1325
1391
  term.loadAddon(
@@ -1581,6 +1647,7 @@ class Term extends Component {
1581
1647
  <DropFileModal
1582
1648
  visible={this.state.dropFileModalVisible}
1583
1649
  files={this.state.droppedFiles}
1650
+ isSerial={this.props.tab?.type === connectionMap.serial}
1584
1651
  onSelect={this.handleDropFileAction}
1585
1652
  onCancel={this.handleDropFileModalCancel}
1586
1653
  />
@@ -0,0 +1,244 @@
1
+ /**
2
+ * XMODEM client handler for web terminal
3
+ * Handles UI interactions and communicates with server-side xmodem
4
+ * Unlike zmodem/trzsz, XMODEM requires explicit user initiation
5
+ */
6
+
7
+ import { throttle } from 'lodash-es'
8
+ import { filesize } from 'filesize'
9
+ import { TransferClientBase } from './transfer-client-base.js'
10
+ import { transferTypeMap } from '../../common/constants.js'
11
+
12
+ const XMODEM_SAVE_PATH_KEY = 'xmodem-save-path'
13
+
14
+ /**
15
+ * XmodemClient class handles XMODEM UI and client-side logic
16
+ */
17
+ export class XmodemClient extends TransferClientBase {
18
+ constructor (terminal) {
19
+ super(terminal, XMODEM_SAVE_PATH_KEY)
20
+ this.transferStartTime = 0
21
+ this.pendingMode = null // 'send' or 'receive' - waiting for user action
22
+ }
23
+
24
+ /**
25
+ * Get the action name for this protocol
26
+ * @returns {string}
27
+ */
28
+ getActionName () {
29
+ return 'xmodem-event'
30
+ }
31
+
32
+ /**
33
+ * Get protocol display name
34
+ * @returns {string}
35
+ */
36
+ getProtocolDisplayName () {
37
+ return 'XMODEM'
38
+ }
39
+
40
+ /**
41
+ * Handle server xmodem events
42
+ * @param {Object} msg - Message from server
43
+ */
44
+ handleServerEvent (msg) {
45
+ const { event } = msg
46
+
47
+ switch (event) {
48
+ case 'receive-start':
49
+ this.onReceiveStart()
50
+ break
51
+ case 'send-start':
52
+ this.onSendStart()
53
+ break
54
+ case 'file-start':
55
+ this.onFileStart(msg.name, msg.size)
56
+ break
57
+ case 'progress':
58
+ this.onProgress(msg)
59
+ break
60
+ case 'file-complete':
61
+ this.onFileComplete(msg.name, msg.path)
62
+ break
63
+ case 'session-end':
64
+ this.onSessionEnd()
65
+ break
66
+ case 'session-error':
67
+ this.onError(msg.error)
68
+ break
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Initiate XMODEM receive from UI
74
+ * User must have already started `sx` (or similar) on the remote device
75
+ */
76
+ async initiateReceive () {
77
+ if (this.isActive) {
78
+ this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
79
+ return
80
+ }
81
+
82
+ this.isActive = true
83
+ this.writeBanner('RECEIVE', null)
84
+
85
+ // Ask user for save directory
86
+ const savePath = await this.openSaveFolderSelect()
87
+ if (savePath) {
88
+ this.savePath = savePath
89
+ this.sendToServer({
90
+ event: 'start-receive'
91
+ })
92
+ // Also send save path
93
+ this.sendToServer({
94
+ event: 'set-save-path',
95
+ path: savePath
96
+ })
97
+ } else {
98
+ // User cancelled
99
+ this.isActive = false
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Initiate XMODEM send from UI
105
+ * User must have already started `rx` (or similar) on the remote device
106
+ */
107
+ async initiateSend () {
108
+ if (this.isActive) {
109
+ this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
110
+ return
111
+ }
112
+
113
+ this.isActive = true
114
+ this.writeBanner('SEND', null)
115
+
116
+ // Ask user to select files
117
+ const files = await this.openFileSelect({
118
+ title: 'Choose file(s) to send via XMODEM',
119
+ message: 'Choose file(s) to send via XMODEM'
120
+ })
121
+ if (files && files.length > 0) {
122
+ this.sendToServer({
123
+ event: 'start-send'
124
+ })
125
+ // Also send files
126
+ this.sendToServer({
127
+ event: 'send-files',
128
+ files
129
+ })
130
+ } else {
131
+ // User cancelled
132
+ this.isActive = false
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Handle receive session start (from server)
138
+ */
139
+ onReceiveStart () {
140
+ // Server confirmed it's ready to receive
141
+ this.writeToTerminal('\r\n\x1b[36mWaiting for remote to start sending file...\x1b[0m\r\n')
142
+ }
143
+
144
+ /**
145
+ * Handle send session start (from server)
146
+ */
147
+ onSendStart () {
148
+ // Server confirmed it's ready to send
149
+ this.writeToTerminal('\r\n\x1b[36mWaiting for remote to request file (NAK/C)...\x1b[0m\r\n')
150
+ }
151
+
152
+ /**
153
+ * Handle file start event
154
+ * @param {string} name - File name
155
+ * @param {number} size - File size
156
+ */
157
+ onFileStart (name, size) {
158
+ this.currentTransfer = {
159
+ name,
160
+ size,
161
+ transferred: 0,
162
+ type: this.savePath ? transferTypeMap.download : transferTypeMap.upload,
163
+ path: null
164
+ }
165
+ this.transferStartTime = Date.now()
166
+ this.writeProgress()
167
+ }
168
+
169
+ /**
170
+ * Handle progress update
171
+ * @param {Object} msg - Progress message
172
+ */
173
+ onProgress (msg) {
174
+ if (!this.currentTransfer) {
175
+ this.currentTransfer = {
176
+ name: msg.name,
177
+ size: msg.size,
178
+ transferred: 0,
179
+ type: msg.type,
180
+ path: msg.path || null
181
+ }
182
+ this.transferStartTime = Date.now()
183
+ }
184
+
185
+ this.currentTransfer.transferred = msg.transferred
186
+ this.currentTransfer.serverSpeed = msg.speed
187
+ this.currentTransfer.path = msg.path || this.currentTransfer.path
188
+ this.writeProgress()
189
+ }
190
+
191
+ /**
192
+ * Handle file complete
193
+ * @param {string} name - File name
194
+ * @param {string} path - File path
195
+ */
196
+ onFileComplete (name, path) {
197
+ if (this.currentTransfer) {
198
+ this.currentTransfer.transferred = this.currentTransfer.size || this.currentTransfer.transferred
199
+ this.currentTransfer.path = path
200
+ this._doWriteProgress(true)
201
+ this.writeToTerminal('\r\n')
202
+ this._prevProgressRows = 0
203
+ }
204
+ this.currentTransfer = null
205
+ }
206
+
207
+ /**
208
+ * Handle error from server
209
+ * @param {string} error - Error message
210
+ */
211
+ onError (error) {
212
+ this.writeToTerminal(`\r\n\x1b[31m\x1b[1mXMODEM Error: ${error}\x1b[0m\r\n`)
213
+ }
214
+
215
+ /**
216
+ * Write progress to terminal (throttled)
217
+ */
218
+ writeProgress = throttle((isComplete = false) => {
219
+ this._doWriteProgress(isComplete)
220
+ }, 500)
221
+
222
+ /**
223
+ * Internal function to actually write progress
224
+ */
225
+ _doWriteProgress (isComplete = false) {
226
+ if (!this.currentTransfer || !this.terminal?.term) return
227
+
228
+ const { name, size, transferred, path, serverSpeed } = this.currentTransfer
229
+ const speed = serverSpeed || 0
230
+ const displayName = path || name
231
+ const formatSize = (bytes) => filesize(bytes)
232
+
233
+ this.writeProgressBar({
234
+ name: displayName,
235
+ size,
236
+ transferred,
237
+ speed,
238
+ isComplete,
239
+ formatSize
240
+ })
241
+ }
242
+ }
243
+
244
+ export default XmodemClient
@@ -24,7 +24,8 @@ 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
+ const parsed = parseInt(txt, 10)
28
+ const r = k === 'mem' ? filesize(Number.isFinite(parsed) ? parsed * 1024 : 0) : txt
28
29
  const itemProps = {
29
30
  className: 'activity-item pointer',
30
31
  'data-content': r,
@@ -14,10 +14,12 @@ export default function TerminalInfoDisk (props) {
14
14
  }
15
15
  const col = colsParser(disks[0])
16
16
  disks.sort((a, b) => {
17
- if (a.filesystem.startsWith('/') && !b.filesystem.startsWith('/')) {
17
+ const af = a.filesystem || ''
18
+ const bf = b.filesystem || ''
19
+ if (af.startsWith('/') && !bf.startsWith('/')) {
18
20
  return -1
19
21
  }
20
- if (!a.filesystem.startsWith('/') && b.filesystem.startsWith('/')) {
22
+ if (!af.startsWith('/') && bf.startsWith('/')) {
21
23
  return 1
22
24
  }
23
25
  return 0
@@ -34,6 +34,7 @@ export default function TerminalInfoDisk (props) {
34
34
  const p = network[k]
35
35
  const pv = net[k]
36
36
  if (
37
+ diff > 0 &&
37
38
  p &&
38
39
  pv &&
39
40
  p.download &&
@@ -43,6 +44,7 @@ export default function TerminalInfoDisk (props) {
43
44
  p.down = Math.floor((p.download - pv.download) / diff)
44
45
  }
45
46
  if (
47
+ diff > 0 &&
46
48
  p &&
47
49
  pv &&
48
50
  p.upload &&
@@ -92,7 +94,7 @@ export default function TerminalInfoDisk (props) {
92
94
  },
93
95
  render: (v) => {
94
96
  if (k === 'up' || k === 'down') {
95
- return filesize(v || 0)
97
+ return filesize(Number.isFinite(v) ? v : 0)
96
98
  }
97
99
  return v
98
100
  }
@@ -2,7 +2,7 @@
2
2
  * cpu/swap/mem general usage
3
3
  */
4
4
 
5
- import { isEmpty, isUndefined } from 'lodash-es'
5
+ import { isEmpty } from 'lodash-es'
6
6
  import { Progress } from 'antd'
7
7
  import parseInt10 from '../../common/parse-int10'
8
8
 
@@ -51,7 +51,7 @@ export default function TerminalInfoResource (props) {
51
51
  percent,
52
52
  name
53
53
  } = obj
54
- const hasPercent = !isUndefined(percent)
54
+ const hasPercent = Number.isFinite(percent)
55
55
  const p = hasPercent
56
56
  ? percent
57
57
  : computePercent(used, total) || 0
@@ -74,7 +74,7 @@ export default function TerminalInfoResource (props) {
74
74
  if (terminalInfos.includes('cpu')) {
75
75
  data.push({
76
76
  name: 'cpu',
77
- percent: parseInt10(cpu)
77
+ percent: parseInt10(cpu) || 0
78
78
  })
79
79
  }
80
80
  if (terminalInfos.includes('mem')) {
@@ -118,10 +118,16 @@
118
118
  visibility hidden
119
119
  &:hover
120
120
  .tree-item-title
121
- padding-right 120px
121
+ padding-right 60px
122
122
  .tree-item-op-wrap
123
123
  opacity 1
124
124
  pointer-events auto
125
+ .add-menu-wrap
126
+ .sidebar-panel
127
+ .tree-list-row
128
+ &:hover
129
+ .tree-item-title
130
+ padding-right 20px
125
131
 
126
132
  .tree-list-row-group
127
133
  position relative