@electerm/electerm-react 3.11.12 → 3.15.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.
Files changed (36) hide show
  1. package/client/common/bookmark-schemas.js +2 -1
  2. package/client/common/constants.js +11 -2
  3. package/client/common/is-absolute-path.js +1 -1
  4. package/client/common/normalize-remote-path.js +20 -0
  5. package/client/common/resolve.js +16 -0
  6. package/client/components/ai/agent-tools.js +204 -0
  7. package/client/components/ai/agent.js +12 -10
  8. package/client/components/ai/ai-chat-history-item.jsx +15 -25
  9. package/client/components/ai/ai-chat.jsx +24 -9
  10. package/client/components/bookmark-form/bookmark-schema.js +2 -1
  11. package/client/components/bookmark-form/config/serial.js +3 -2
  12. package/client/components/footer/cmd-history.jsx +20 -11
  13. package/client/components/main/main.jsx +1 -1
  14. package/client/components/sftp/address-bar.jsx +22 -6
  15. package/client/components/sftp/file-item.jsx +31 -1
  16. package/client/components/sftp/file-read.js +11 -2
  17. package/client/components/sftp/sftp-entry.jsx +38 -3
  18. package/client/components/shortcuts/shortcut-handler.js +7 -0
  19. package/client/components/sidebar/history.jsx +16 -8
  20. package/client/components/sys-menu/icons-map.jsx +10 -2
  21. package/client/components/terminal/attach-addon-custom.js +1 -1
  22. package/client/components/terminal/drop-file-modal.jsx +53 -22
  23. package/client/components/terminal/terminal-apis.js +9 -0
  24. package/client/components/terminal/terminal.jsx +179 -3
  25. package/client/components/terminal/xmodem-client.js +244 -0
  26. package/client/components/terminal-info/base.jsx +41 -38
  27. package/client/components/terminal-info/data-cols-parser.jsx +2 -1
  28. package/client/components/terminal-info/disk.jsx +4 -2
  29. package/client/components/terminal-info/log-path-edit.jsx +3 -2
  30. package/client/components/terminal-info/network.jsx +3 -1
  31. package/client/components/terminal-info/resource.jsx +3 -3
  32. package/client/components/tree-list/tree-list.styl +7 -1
  33. package/client/store/mcp-handler.js +41 -9
  34. package/client/store/quick-command.js +3 -2
  35. package/client/store/watch.js +1 -1
  36. package/package.json +1 -1
@@ -7,6 +7,8 @@ import {
7
7
  Dropdown
8
8
  } from 'antd'
9
9
  import message from '../common/message'
10
+ import { notification } from '../common/notification'
11
+ import ShowItem from '../common/show-item.jsx'
10
12
  import Modal from '../common/modal'
11
13
  import classnames from 'classnames'
12
14
  import './terminal.styl'
@@ -26,13 +28,15 @@ import AttachAddon from './attach-addon-custom.js'
26
28
  import getProxy from '../../common/get-proxy.js'
27
29
  import { ZmodemClient } from './zmodem-client.js'
28
30
  import { TrzszClient } from './trzsz-client.js'
31
+ import { XmodemClient } from './xmodem-client.js'
29
32
  import DropFileModal from './drop-file-modal.jsx'
30
33
  import keyControlPressed from '../../common/key-control-pressed.js'
31
34
  import NormalBuffer from './normal-buffer.jsx'
32
- import { createTerm, resizeTerm } from './terminal-apis.js'
35
+ import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
33
36
  import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
34
37
  import { KeywordHighlighterAddon } from './highlight-addon.js'
35
38
  import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
39
+ import { getFolderFromFilePath } from '../sftp/file-read.js'
36
40
  import { CommandTrackerAddon } from './command-tracker-addon.js'
37
41
  import AIIcon from '../icons/ai-icon.jsx'
38
42
  import {
@@ -71,6 +75,9 @@ class Term extends Component {
71
75
  saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
72
76
  addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
73
77
  logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
78
+ logFileName: '',
79
+ recording: false,
80
+ recordingFilePath: '',
74
81
  passType: 'password',
75
82
  lines: [],
76
83
  searchResults: [],
@@ -165,6 +172,7 @@ class Term extends Component {
165
172
  this.fitAddon = null
166
173
  this.zmodemClient = null
167
174
  this.trzszClient = null
175
+ this.xmodemClient = null
168
176
  this.searchAddon = null
169
177
  this.fitAddon = null
170
178
  this.cmdAddon = null
@@ -391,6 +399,7 @@ class Term extends Component {
391
399
  const fromFile = dt.getData('fromFile')
392
400
  const notSafeMsg = 'File name contains unsafe characters'
393
401
  const isSshTerminal = this.props.tab.type === connectionMap.ssh
402
+ const isSerialTerminal = this.props.tab.type === connectionMap.serial
394
403
 
395
404
  if (fromFile) {
396
405
  try {
@@ -412,6 +421,13 @@ class Term extends Component {
412
421
  }
413
422
  return
414
423
  }
424
+ if (isSerialTerminal) {
425
+ this.setState({
426
+ dropFileModalVisible: true,
427
+ droppedFiles: [{ path: filePath, isRemote: false }]
428
+ })
429
+ return
430
+ }
415
431
  this.attachAddon._sendData(`"${filePath}" `)
416
432
  return
417
433
  } catch (e) {
@@ -443,6 +459,14 @@ class Term extends Component {
443
459
  return
444
460
  }
445
461
 
462
+ if (isSerialTerminal) {
463
+ this.setState({
464
+ dropFileModalVisible: true,
465
+ droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
466
+ })
467
+ return
468
+ }
469
+
446
470
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
447
471
  this.attachAddon._sendData(filesAll)
448
472
  }
@@ -485,6 +509,19 @@ class Term extends Component {
485
509
  this.attachAddon._sendData('rz\r')
486
510
  break
487
511
  }
512
+ case 'xmodem': {
513
+ if (this.xmodemClient && this.xmodemClient.isActive) {
514
+ message.warning('A transfer is already in progress')
515
+ this.handleDropFileModalCancel()
516
+ return
517
+ }
518
+ // Use XMODEM send with the dropped files
519
+ window._apiControlSelectFile = filePaths
520
+ if (this.xmodemClient) {
521
+ this.xmodemClient.initiateSend()
522
+ }
523
+ break
524
+ }
488
525
  case 'inputOnly':
489
526
  default: {
490
527
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
@@ -645,15 +682,109 @@ class Term extends Component {
645
682
  )
646
683
  }
647
684
 
685
+ getTerminalBufferText = () => {
686
+ const { addTimeStampToTermLog } = this.state
687
+ const buffer = this.term.buffer.active
688
+ const len = buffer.length
689
+ const rawLines = []
690
+ for (let i = 0; i < len; i++) {
691
+ const line = buffer.getLine(i)
692
+ rawLines.push(line ? line.translateToString(false) : '')
693
+ }
694
+ // trim trailing blank lines before applying timestamps
695
+ while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
696
+ rawLines.pop()
697
+ }
698
+ if (!addTimeStampToTermLog) {
699
+ return rawLines.join('\n')
700
+ }
701
+ return rawLines.map(text => {
702
+ const now = new Date()
703
+ const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
704
+ return ts + text
705
+ }).join('\n')
706
+ }
707
+
708
+ syncTermInfo = (stateUpdate) => {
709
+ this.setState(stateUpdate)
710
+ const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
711
+ if (Object.keys(infoUpdate).length) {
712
+ refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
713
+ }
714
+ }
715
+
716
+ openLogSaveDialog = async (titleKey) => {
717
+ const { logName } = this.props
718
+ const result = await window.api.saveDialog({
719
+ title: e(titleKey),
720
+ defaultPath: logName + '.log',
721
+ filters: [
722
+ { name: 'Log files', extensions: ['log'] }
723
+ ],
724
+ properties: ['createDirectory', 'showOverwriteConfirmation']
725
+ })
726
+ if (result.canceled || !result.filePath) {
727
+ return null
728
+ }
729
+ return result.filePath
730
+ }
731
+
732
+ onSaveTerminalLog = async () => {
733
+ const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
734
+ if (!filePath) {
735
+ return
736
+ }
737
+ const content = this.getTerminalBufferText()
738
+ await window.fs.writeFile(filePath, content).catch(window.store.onError)
739
+ const { addTimeStampToTermLog } = this.state
740
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
741
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
742
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
743
+ notification.success({
744
+ message: e('saveTerminalLogToFile'),
745
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
746
+ duration: 5
747
+ })
748
+ }
749
+
750
+ onRecord = async () => {
751
+ const filePath = await this.openLogSaveDialog('record')
752
+ if (!filePath) {
753
+ return
754
+ }
755
+ const { addTimeStampToTermLog } = this.state
756
+ startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
757
+ const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
758
+ this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
759
+ this.setState({ recording: true, recordingFilePath: filePath })
760
+ notification.success({
761
+ message: e('record'),
762
+ description: <ShowItem to={filePath}>{filePath}</ShowItem>,
763
+ duration: 5
764
+ })
765
+ }
766
+
767
+ onStopRecord = () => {
768
+ const { recordingFilePath } = this.state
769
+ toggleTerminalLog(this.pid).catch(window.store.onError)
770
+ this.syncTermInfo({ saveTerminalLogToFile: false })
771
+ this.setState({ recording: false, recordingFilePath: '' })
772
+ notification.success({
773
+ message: e('stopRecord'),
774
+ description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
775
+ })
776
+ }
777
+
648
778
  renderContextMenu = () => {
649
- const { hasSelection } = this.state
779
+ const { hasSelection, recording } = this.state
650
780
  const copyed = true
651
781
  const copyShortcut = this.getShortcut('terminal_copy')
652
782
  const pasteShortcut = this.getShortcut('terminal_paste')
653
783
  const clearShortcut = this.getShortcut('terminal_clear')
654
784
  const searchShortcut = this.getShortcut('terminal_search')
655
785
  const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
656
- return [
786
+ const isSerial = this.props.tab?.type === connectionMap.serial
787
+ const items = [
657
788
  {
658
789
  key: 'onCopy',
659
790
  icon: <iconsMap.CopyOutlined />,
@@ -698,14 +829,56 @@ class Term extends Component {
698
829
  icon: <iconsMap.SearchOutlined />,
699
830
  label: e('search'),
700
831
  extra: searchShortcut
832
+ },
833
+ {
834
+ key: 'onSaveTerminalLog',
835
+ icon: <iconsMap.SaveOutlined />,
836
+ label: e('saveTerminalLogToFile')
837
+ },
838
+ {
839
+ key: recording ? 'onStopRecord' : 'onRecord',
840
+ icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
841
+ label: e(recording ? 'stopRecord' : 'record')
701
842
  }
702
843
  ]
844
+ if (isSerial) {
845
+ items.push(
846
+ {
847
+ type: 'divider'
848
+ },
849
+ {
850
+ key: 'onXmodemSend',
851
+ icon: <iconsMap.CloudUploadOutlined />,
852
+ label: 'XMODEM Send'
853
+ },
854
+ {
855
+ key: 'onXmodemReceive',
856
+ icon: <iconsMap.CloudDownloadOutlined />,
857
+ label: 'XMODEM Receive'
858
+ }
859
+ )
860
+ }
861
+ return items
703
862
  }
704
863
 
705
864
  onContextMenu = ({ key }) => {
706
865
  this[key]()
707
866
  }
708
867
 
868
+ onXmodemSend = () => {
869
+ if (this.xmodemClient) {
870
+ this.xmodemClient.initiateSend()
871
+ }
872
+ this.term.focus()
873
+ }
874
+
875
+ onXmodemReceive = () => {
876
+ if (this.xmodemClient) {
877
+ this.xmodemClient.initiateReceive()
878
+ }
879
+ this.term.focus()
880
+ }
881
+
709
882
  notifyOnData = debounce(() => {
710
883
  window.store.notifyTabOnData(this.props.tab.id)
711
884
  }, 1000)
@@ -1320,6 +1493,8 @@ class Term extends Component {
1320
1493
  this.zmodemClient.init(socket)
1321
1494
  this.trzszClient = new TrzszClient(this)
1322
1495
  this.trzszClient.init(socket)
1496
+ this.xmodemClient = new XmodemClient(this)
1497
+ this.xmodemClient.init(socket)
1323
1498
  this.fitAddon.fit()
1324
1499
  term.displayRaw = displayRaw
1325
1500
  term.loadAddon(
@@ -1581,6 +1756,7 @@ class Term extends Component {
1581
1756
  <DropFileModal
1582
1757
  visible={this.state.dropFileModalVisible}
1583
1758
  files={this.state.droppedFiles}
1759
+ isSerial={this.props.tab?.type === connectionMap.serial}
1584
1760
  onSelect={this.handleDropFileAction}
1585
1761
  onCancel={this.handleDropFileModalCancel}
1586
1762
  />
@@ -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
@@ -8,7 +8,7 @@ import {
8
8
  Button
9
9
  } from 'antd'
10
10
  import defaults from '../../common/default-setting'
11
- import { toggleTerminalLog, toggleTerminalLogTimestamp, setTerminalLogPath } from '../terminal/terminal-apis'
11
+ import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
12
12
  import {
13
13
  ClockCircleOutlined,
14
14
  BorderlessTableOutlined,
@@ -18,7 +18,9 @@ import {
18
18
  PartitionOutlined
19
19
  } from '@ant-design/icons'
20
20
  import { refs } from '../common/ref'
21
- import LogPathEdit from './log-path-edit'
21
+ import ShowItem from '../common/show-item'
22
+ import { osResolve } from '../../common/resolve'
23
+ import createDefaultLogPath from '../../common/default-log-path'
22
24
 
23
25
  const e = window.translate
24
26
 
@@ -35,15 +37,20 @@ export default class TerminalInfoBase extends Component {
35
37
  state = {
36
38
  saveTerminalLogToFile: false,
37
39
  addTimeStampToTermLog: false,
38
- logPath: ''
40
+ logPath: '',
41
+ logFileName: ''
39
42
  }
40
43
 
41
44
  componentDidMount () {
45
+ const { pid } = this.props
46
+ refs.add('term-info-' + pid, this)
42
47
  this.getState()
43
48
  }
44
49
 
45
50
  componentWillUnmount () {
46
51
  clearTimeout(this.timer)
52
+ const { pid } = this.props
53
+ refs.remove('term-info-' + pid)
47
54
  }
48
55
 
49
56
  handleToggleTimestamp = () => {
@@ -74,17 +81,6 @@ export default class TerminalInfoBase extends Component {
74
81
  })
75
82
  }
76
83
 
77
- onLogPathChange = (v) => {
78
- const { pid } = this.props
79
- setTerminalLogPath(pid, v)
80
- refs.get('term-' + pid)?.setState({
81
- logPath: v
82
- })
83
- this.setState({
84
- logPath: v
85
- })
86
- }
87
-
88
84
  handleToggle = () => {
89
85
  const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
90
86
  const {
@@ -112,7 +108,8 @@ export default class TerminalInfoBase extends Component {
112
108
  this.setState({
113
109
  saveTerminalLogToFile: term.state.saveTerminalLogToFile,
114
110
  addTimeStampToTermLog: term.state.addTimeStampToTermLog,
115
- logPath: term.state.logPath
111
+ logPath: term.state.logPath,
112
+ logFileName: term.state.logFileName || ''
116
113
  })
117
114
  } else {
118
115
  this.timer = setTimeout(this.getState, 100)
@@ -166,39 +163,45 @@ export default class TerminalInfoBase extends Component {
166
163
  render () {
167
164
  const {
168
165
  id,
169
- logName,
170
- pid
166
+ logName
171
167
  } = this.props
172
- const { saveTerminalLogToFile, logPath } = this.state
168
+ const { saveTerminalLogToFile, logPath, logFileName } = this.state
173
169
  const name = e('saveTerminalLogToFile')
170
+ const base = logPath || createDefaultLogPath()
171
+ const fileName = logFileName || (logName + '.log')
172
+ const fullPath = osResolve(base, fileName)
174
173
  return (
175
174
  <div className='terminal-info-section terminal-info-base'>
176
- <div className='fix'>
177
- <span className='fleft'><b>ID:</b> {id}</span>
178
- <span className='fright'>
179
- <Switch
180
- checkedChildren={name}
181
- unCheckedChildren={name}
182
- checked={saveTerminalLogToFile}
183
- onChange={this.handleToggle}
184
- className='mg1r mg1b'
185
- />
186
- {
187
- this.renderTimestamp()
188
- }
189
- </span>
175
+ <div className='pd1b'>
176
+ <b>ID:</b> {id}
177
+ </div>
178
+ <div className='pd1b'>
179
+ <Switch
180
+ checkedChildren={name}
181
+ unCheckedChildren={name}
182
+ checked={saveTerminalLogToFile}
183
+ onChange={this.handleToggle}
184
+ className='mg1r mg1b'
185
+ />
186
+ {
187
+ this.renderTimestamp()
188
+ }
190
189
  </div>
190
+ {
191
+ saveTerminalLogToFile
192
+ ? (
193
+ <div className='pd1b font-xs color-grey'>
194
+ {e('terminalLogPath')}: {fullPath} <ShowItem to={fullPath} />
195
+ </div>
196
+ )
197
+ : null
198
+ }
191
199
  <div className='pd2y'>
192
200
  {
193
201
  this.renderInfoSelection()
194
202
  }
195
203
  </div>
196
- <LogPathEdit
197
- pid={pid}
198
- logPath={logPath}
199
- logName={logName}
200
- setLogPath={this.onLogPathChange}
201
- />
204
+
202
205
  </div>
203
206
  )
204
207
  }
@@ -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
@@ -12,10 +12,11 @@ import { osResolve } from '../../common/resolve'
12
12
 
13
13
  const e = window.translate
14
14
 
15
- export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
15
+ export default function LogPathEdit ({ pid, logPath, logName, logFileName, setLogPath }) {
16
16
  const defaultPath = createDefaultLogPath()
17
17
  const base = logPath || defaultPath
18
- const fullPath = osResolve(base, logName + '.log')
18
+ const fileName = logFileName || (logName + '.log')
19
+ const fullPath = osResolve(base, fileName)
19
20
 
20
21
  const testAndSet = async (v) => {
21
22
  if (v) {