@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
@@ -105,17 +105,11 @@ export default auto(function CmdHistory (props) {
105
105
  ))
106
106
  }
107
107
 
108
- const content = (
109
- <div className='cmd-history-popover-content pd2'>
110
- <div className='cmd-history-search pd2b'>
111
- <InputAutoFocus
112
- value={keyword}
113
- onChange={handleChange}
114
- placeholder={e('search')}
115
- className='cmd-history-search-input'
116
- allowClear
117
- />
118
- </div>
108
+ function renderHeader () {
109
+ if (!historyArray.length) {
110
+ return null
111
+ }
112
+ return (
119
113
  <div className='cmd-history-header pd2b'>
120
114
  <Switch
121
115
  checkedChildren={e('sortByFrequency')}
@@ -130,6 +124,21 @@ export default auto(function CmdHistory (props) {
130
124
  onClick={handleClearAll}
131
125
  />
132
126
  </div>
127
+ )
128
+ }
129
+
130
+ const content = (
131
+ <div className='cmd-history-popover-content pd2'>
132
+ <div className='cmd-history-search pd2b'>
133
+ <InputAutoFocus
134
+ value={keyword}
135
+ onChange={handleChange}
136
+ placeholder={e('search')}
137
+ className='cmd-history-search-input'
138
+ allowClear
139
+ />
140
+ </div>
141
+ {renderHeader()}
133
142
  <div className='cmd-history-list'>
134
143
  {renderList()}
135
144
  </div>
@@ -282,7 +282,7 @@ export default auto(function Index (props) {
282
282
  <InfoModal {...infoModalProps} />
283
283
  <RightSidePanel {...rightPanelProps}>
284
284
  <AIChat {...aiChatProps} />
285
- <TerminalInfo {...terminalInfoProps} />
285
+ <TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
286
286
  </RightSidePanel>
287
287
  <SshConfigLoadNotify {...sshConfigProps} />
288
288
  <LoadSshConfigs
@@ -6,7 +6,8 @@ import {
6
6
  ReloadOutlined,
7
7
  ArrowRightOutlined,
8
8
  LoadingOutlined,
9
- HomeOutlined
9
+ HomeOutlined,
10
+ PlusOutlined
10
11
  } from '@ant-design/icons'
11
12
  import {
12
13
  Input,
@@ -74,7 +75,7 @@ function renderAddonBefore (props, realPath) {
74
75
  )
75
76
  }
76
77
 
77
- function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
78
+ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type, handleUploadFromBrowser) {
78
79
  const handleClick = (e) => {
79
80
  e.stopPropagation()
80
81
  if (!isLoadingRemote) {
@@ -82,9 +83,24 @@ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
82
83
  }
83
84
  }
84
85
  return (
85
- <GoIcon
86
- onClick={handleClick}
87
- />
86
+ <>
87
+ {
88
+ type === typeMap.local && window.et.isWebApp
89
+ ? (
90
+ <PlusOutlined
91
+ className='mg1r'
92
+ onClick={(e) => {
93
+ e.stopPropagation()
94
+ handleUploadFromBrowser()
95
+ }}
96
+ />
97
+ )
98
+ : null
99
+ }
100
+ <GoIcon
101
+ onClick={handleClick}
102
+ />
103
+ </>
88
104
  )
89
105
  }
90
106
 
@@ -160,7 +176,7 @@ export default function AddressBar (props) {
160
176
  onBlur={() => props.onInputBlur(type)}
161
177
  disabled={loadingSftp}
162
178
  suffix={
163
- renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type)
179
+ renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type, props.handleUploadFromBrowser)
164
180
  }
165
181
  />
166
182
  {renderHistory(props, type)}
@@ -14,6 +14,7 @@ import copy from 'json-deep-copy'
14
14
  import { pick, some } from 'lodash-es'
15
15
  import Input from '../common/input-auto-focus'
16
16
  import resolve from '../../common/resolve'
17
+ import normalizeRemotePath from '../../common/normalize-remote-path'
17
18
  import { addClass, removeClass } from '../../common/class'
18
19
  import {
19
20
  mode2permission,
@@ -604,7 +605,10 @@ export default class FileSection extends React.Component {
604
605
  const { type, name, isParent } = file
605
606
  const n = `${type}Path`
606
607
  const path = isParent ? file.path : this.props[n]
607
- const np = resolve(path, name)
608
+ let np = resolve(path, name)
609
+ if (type === typeMap.remote) {
610
+ np = normalizeRemotePath(np)
611
+ }
608
612
  const op = this.props[type + 'Path']
609
613
  this.props.modifier({
610
614
  [n]: np,
@@ -882,6 +886,25 @@ export default class FileSection extends React.Component {
882
886
  window.pre.showItemInFolder(p)
883
887
  }
884
888
 
889
+ downloadFromBrowser = async () => {
890
+ const { path, name, isDirectory } = this.state.file
891
+ const p = resolve(path, name)
892
+ const url = '/api/download?path=' + encodeURIComponent(p)
893
+ const res = await window.fetch(url, {
894
+ headers: {
895
+ token: window.store?.config.tokenElecterm
896
+ }
897
+ })
898
+ const blob = await res.blob()
899
+ const a = document.createElement('a')
900
+ a.href = URL.createObjectURL(blob)
901
+ a.download = isDirectory ? name + '.tar.gz' : name
902
+ document.body.appendChild(a)
903
+ a.click()
904
+ document.body.removeChild(a)
905
+ URL.revokeObjectURL(a.href)
906
+ }
907
+
885
908
  newItem = (isDirectory) => {
886
909
  const { type } = this.state.file
887
910
  const list = copy(this.props[type])
@@ -1060,6 +1083,13 @@ export default class FileSection extends React.Component {
1060
1083
  text: e('showInDefaultFileMananger')
1061
1084
  })
1062
1085
  }
1086
+ if (isLocal && isRealFile && window.et.isWebApp) {
1087
+ res.push({
1088
+ func: 'downloadFromBrowser',
1089
+ icon: 'DownloadOutlined',
1090
+ text: 'Download from browser'
1091
+ })
1092
+ }
1063
1093
  if (showEdit) {
1064
1094
  res.push({
1065
1095
  func: 'editFile',
@@ -7,6 +7,12 @@ import fs from '../../common/fs'
7
7
  import { isWin } from '../../common/constants'
8
8
 
9
9
  export const getFileExt = fileName => {
10
+ if (/^\\\\(?:wsl\$|wsl\.localhost)\\/.test(fileName)) {
11
+ return {
12
+ base: fileName,
13
+ ext: ''
14
+ }
15
+ }
10
16
  const sep = '.'
11
17
  const arr = fileName.split(sep)
12
18
  const len = arr.length
@@ -82,12 +88,15 @@ export const getFolderFromFilePath = (filePath, isRemote) => {
82
88
  const arr = filePath.split(sep)
83
89
  const len = arr.length
84
90
  const isWinDisk = isWin && filePath.endsWith(sep)
85
- const path = isWinDisk
91
+ const isWslRoot = isWin && /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(filePath.replace(/\\$/, ''))
92
+ const path = (isWinDisk || isWslRoot)
86
93
  ? '/'
87
94
  : arr.slice(0, len - 1).join(sep)
88
95
  const name = isWinDisk
89
96
  ? filePath.replace(sep, '')
90
- : arr[len - 1]
97
+ : isWslRoot
98
+ ? filePath
99
+ : arr[len - 1]
91
100
 
92
101
  return {
93
102
  path,
@@ -29,6 +29,7 @@ import fs from '../../common/fs'
29
29
  import ListTable from './list-table-ui'
30
30
  import deepCopy from 'json-deep-copy'
31
31
  import isValidPath from '../../common/is-valid-path'
32
+ import normalizeRemotePath from '../../common/normalize-remote-path'
32
33
  import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
33
34
  import * as owner from './owner-list'
34
35
  import AddressBar from './address-bar'
@@ -292,6 +293,7 @@ export default class Sftp extends Component {
292
293
  if (!path && this.sftp) {
293
294
  path = await this.getPwd(this.props.tab.username)
294
295
  }
296
+ path = normalizeRemotePath(path)
295
297
  } else {
296
298
  path = this.getLocalHome()
297
299
  }
@@ -762,7 +764,7 @@ export default class Sftp extends Component {
762
764
 
763
765
  if (!remotePath) {
764
766
  if (startDirectory) {
765
- remotePath = startDirectory
767
+ remotePath = normalizeRemotePath(startDirectory)
766
768
  } else {
767
769
  remotePath = await this.getPwd(username)
768
770
  }
@@ -970,6 +972,31 @@ export default class Sftp extends Component {
970
972
  })
971
973
  }
972
974
 
975
+ handleUploadFromBrowser = () => {
976
+ const input = document.createElement('input')
977
+ input.type = 'file'
978
+ input.multiple = true
979
+ input.onchange = async () => {
980
+ const files = input.files
981
+ if (!files || !files.length) return
982
+ const { localPath } = this.state
983
+ for (const file of files) {
984
+ const formData = new FormData()
985
+ formData.append('file', file)
986
+ formData.append('path', localPath)
987
+ await window.fetch('/api/upload', {
988
+ method: 'POST',
989
+ body: formData,
990
+ headers: {
991
+ token: window.store?.config.tokenElecterm
992
+ }
993
+ })
994
+ }
995
+ this.localList()
996
+ }
997
+ input.click()
998
+ }
999
+
973
1000
  parsePath = async (type, pth) => {
974
1001
  const reg = /^%([^%]+)%/
975
1002
  if (!reg.test(pth)) {
@@ -995,7 +1022,10 @@ export default class Sftp extends Component {
995
1022
  const n = `${type}Path`
996
1023
  const nt = n + 'Temp'
997
1024
  const oldPath = this.state[type + 'Path']
998
- const np = await this.parsePath(type, this.state[nt])
1025
+ let np = await this.parsePath(type, this.state[nt])
1026
+ if (type === typeMap.remote) {
1027
+ np = normalizeRemotePath(np)
1028
+ }
999
1029
  if (!isValidPath(np)) {
1000
1030
  return notification.warning({
1001
1031
  message: 'path not valid'
@@ -1003,6 +1033,7 @@ export default class Sftp extends Component {
1003
1033
  }
1004
1034
  this.setState({
1005
1035
  [n]: np,
1036
+ [nt]: np,
1006
1037
  [`${type}Keyword`]: ''
1007
1038
  }, () => this[`${type}List`](undefined, undefined, oldPath))
1008
1039
  }
@@ -1010,7 +1041,10 @@ export default class Sftp extends Component {
1010
1041
  goParent = (type) => {
1011
1042
  const n = `${type}Path`
1012
1043
  const p = this.state[n]
1013
- const np = resolve(p, '..')
1044
+ let np = resolve(p, '..')
1045
+ if (type === typeMap.remote) {
1046
+ np = normalizeRemotePath(np)
1047
+ }
1014
1048
  const op = this.state[n]
1015
1049
  if (np !== p) {
1016
1050
  this.setState({
@@ -1218,6 +1252,7 @@ export default class Sftp extends Component {
1218
1252
  const addrProps = {
1219
1253
  host,
1220
1254
  type,
1255
+ handleUploadFromBrowser: this.handleUploadFromBrowser,
1221
1256
  ...pick(
1222
1257
  this,
1223
1258
  [
@@ -82,6 +82,7 @@ export function handleTerminalSelectionReplace (event, ctx) {
82
82
  }
83
83
 
84
84
  ctx.term.clearSelection()
85
+ ctx.term.scrollToBottom()
85
86
  return true
86
87
  }
87
88
 
@@ -167,6 +168,7 @@ export function shortcutExtend (Cls) {
167
168
  const altDelDelKey = delKey === 8 ? 127 : 8
168
169
  const char = String.fromCharCode(shiftKey ? delKey : altDelDelKey)
169
170
  this.socket.send(char)
171
+ this.term.scrollToBottom()
170
172
  return false
171
173
  } else if (
172
174
  this.term &&
@@ -193,6 +195,11 @@ export function shortcutExtend (Cls) {
193
195
  this.trzszClient.cancel()
194
196
  return false
195
197
  }
198
+ // Cancel xmodem transfer if active
199
+ if (this.xmodemClient && this.xmodemClient.isActive) {
200
+ this.xmodemClient.cancel()
201
+ return false
202
+ }
196
203
  }
197
204
 
198
205
  let codeName
@@ -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) => {
@@ -24,7 +24,11 @@ import {
24
24
  LockOutlined,
25
25
  ReloadOutlined,
26
26
  FileZipOutlined,
27
- AppstoreOutlined
27
+ AppstoreOutlined,
28
+ SaveOutlined,
29
+ PlayCircleFilled,
30
+ StopOutlined,
31
+ DownloadOutlined
28
32
  } from '@ant-design/icons'
29
33
  import IconHolder from './icon-holder'
30
34
 
@@ -52,5 +56,9 @@ export default {
52
56
  LockOutlined,
53
57
  ReloadOutlined,
54
58
  FileZipOutlined,
55
- AppstoreOutlined
59
+ AppstoreOutlined,
60
+ SaveOutlined,
61
+ PlayCircleFilled,
62
+ StopOutlined,
63
+ DownloadOutlined
56
64
  }
@@ -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}
@@ -49,3 +49,12 @@ export function setTerminalLogPath (pid, logPath) {
49
49
  action: 'set-terminal-log-path'
50
50
  })
51
51
  }
52
+
53
+ export function startTerminalLogFile (pid, logFilePath, addTimeStampToTermLog) {
54
+ return fetch({
55
+ pid,
56
+ logFilePath,
57
+ addTimeStampToTermLog,
58
+ action: 'start-terminal-log-file'
59
+ })
60
+ }