@electerm/electerm-react 1.38.11 → 1.38.30

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 (27) hide show
  1. package/client/common/build-ssh-tunnel.js +2 -1
  2. package/client/common/default-setting.js +9 -1
  3. package/client/components/bookmark-form/render-ssh-tunnel.jsx +32 -14
  4. package/client/components/main/main.jsx +4 -0
  5. package/client/components/quick-commands/quick-commands-box.jsx +20 -5
  6. package/client/components/quick-commands/quick-commands-form-elem.jsx +14 -17
  7. package/client/components/quick-commands/quick-commands-list-form.jsx +89 -0
  8. package/client/components/session/session.jsx +5 -4
  9. package/client/components/setting-panel/keywords-form.jsx +2 -1
  10. package/client/components/setting-panel/setting-terminal.jsx +5 -2
  11. package/client/components/setting-sync/setting-sync-form.jsx +1 -3
  12. package/client/components/sftp/file-item.jsx +27 -20
  13. package/client/components/sftp/sftp-entry.jsx +65 -45
  14. package/client/components/tabs/index.jsx +1 -1
  15. package/client/components/terminal/attach-addon-custom.js +2 -2
  16. package/client/components/terminal/index.jsx +3 -3
  17. package/client/components/terminal/xterm-zmodem.js +3 -3
  18. package/client/components/terminal-info/activity.jsx +2 -2
  19. package/client/components/terminal-info/base.jsx +47 -2
  20. package/client/components/terminal-info/disk.jsx +2 -2
  21. package/client/components/terminal-info/network.jsx +2 -2
  22. package/client/components/terminal-info/resource.jsx +20 -10
  23. package/client/components/terminal-info/up.jsx +2 -2
  24. package/client/css/basic.styl +3 -0
  25. package/client/store/common.js +51 -0
  26. package/client/store/system-menu.js +6 -16
  27. package/package.json +1 -1
@@ -2,6 +2,7 @@ export const buildSshTunnels = function (inst) {
2
2
  return [{
3
3
  sshTunnel: inst.sshTunnel,
4
4
  sshTunnelRemotePort: inst.sshTunnelRemotePort,
5
- sshTunnelLocalPort: inst.sshTunnelLocalPort
5
+ sshTunnelLocalPort: inst.sshTunnelLocalPort,
6
+ sshTunnelRemoteHost: inst.sshTunnelRemoteHost
6
7
  }]
7
8
  }
@@ -43,5 +43,13 @@ export default {
43
43
  addTimeStampToTermLog: false,
44
44
  sftpPathFollowSsh: false,
45
45
  keepaliveInterval: 0,
46
- backspaceMode: '^?'
46
+ backspaceMode: '^?',
47
+ terminalInfos: [
48
+ 'uptime',
49
+ 'cpu',
50
+ 'mem',
51
+ 'activities',
52
+ 'network',
53
+ 'disks'
54
+ ]
47
55
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  Form,
3
+ Input,
3
4
  InputNumber,
4
5
  Radio,
5
6
  Space,
@@ -47,29 +48,46 @@ export default function renderSshTunnel () {
47
48
  </RadioButton>
48
49
  </RadioGroup>
49
50
  </FormItem>
51
+ <Space.Compact className='mg2x'>
52
+ <FormItem
53
+ label={e('destination')}
54
+ name={[field.name, 'sshTunnelRemoteHost']}
55
+ initialValue='127.0.0.1'
56
+ required
57
+ >
58
+ <Input
59
+ className='compact-input'
60
+ placeholder={e('host')}
61
+ />
62
+ </FormItem>
63
+ <FormItem
64
+ label=''
65
+ name={[field.name, 'sshTunnelRemotePort']}
66
+ initialValue={22}
67
+ required
68
+ >
69
+ <InputNumber
70
+ min={1}
71
+ max={65535}
72
+ // addonBefore={e('remotePort')}
73
+ className='compact-input'
74
+ placeholder={e('port')}
75
+ />
76
+ </FormItem>
77
+ </Space.Compact>
50
78
  <FormItem
51
- label=''
52
- name={[field.name, 'sshTunnelRemotePort']}
53
- required
54
- >
55
- <InputNumber
56
- min={1}
57
- max={65535}
58
- addonBefore={e('remotePort')}
59
- className='compact-input'
60
- />
61
- </FormItem>
62
- <FormItem
63
- label=''
79
+ label={e('localPort')}
64
80
  name={[field.name, 'sshTunnelLocalPort']}
81
+ initialValue={22}
65
82
  required
66
83
  className='mg2x'
67
84
  >
68
85
  <InputNumber
69
86
  min={1}
70
87
  max={65535}
71
- addonBefore={e('localPort')}
88
+ // addonBefore={e('localPort')}
72
89
  className='compact-input'
90
+ placeholder={e('port')}
73
91
  />
74
92
  </FormItem>
75
93
  <Button
@@ -41,6 +41,7 @@ export default class Index extends Component {
41
41
  ipcOnEvent('zoom-reset', store.onZoomReset)
42
42
  ipcOnEvent('zoomin', store.onZoomIn)
43
43
  ipcOnEvent('zoomout', store.onZoomout)
44
+ ipcOnEvent('confirm-exit', store.beforeExitApp)
44
45
 
45
46
  document.addEventListener('drop', function (e) {
46
47
  e.preventDefault()
@@ -51,6 +52,9 @@ export default class Index extends Component {
51
52
  e.stopPropagation()
52
53
  })
53
54
  window.addEventListener('offline', store.setOffline)
55
+ if (window.et.isWebApp) {
56
+ window.onbeforeunload = store.beforeExit
57
+ }
54
58
  store.isSencondInstance = window.pre.runSync('isSencondInstance')
55
59
  store.initData()
56
60
  store.checkForDbUpgrade()
@@ -8,7 +8,9 @@ import { find, sortBy } from 'lodash-es'
8
8
  import { Button, Input, Select, Space } from 'antd'
9
9
  import * as ls from '../../common/safe-local-storage'
10
10
  import copy from 'json-deep-copy'
11
+ import generate from '../../common/uid'
11
12
  import CmdItem from './quick-command-item'
13
+ import delay from '../../common/wait'
12
14
  import {
13
15
  EditOutlined,
14
16
  CloseCircleOutlined,
@@ -46,7 +48,7 @@ export default class QuickCommandsFooterBox extends Component {
46
48
  this.props.store.pinnedQuickCommandBar = !this.props.store.pinnedQuickCommandBar
47
49
  }
48
50
 
49
- handleSelect = (id) => {
51
+ handleSelect = async (id) => {
50
52
  const {
51
53
  store
52
54
  } = this.props
@@ -57,11 +59,24 @@ export default class QuickCommandsFooterBox extends Component {
57
59
  this.props.store.currentQuickCommands,
58
60
  a => a.id === id
59
61
  )
60
- if (qm && qm.command) {
61
- const { runQuickCommand } = this.props.store
62
+ const { runQuickCommand } = this.props.store
63
+ const qms = qm && qm.commands
64
+ ? qm.commands
65
+ : (qm && qm.command
66
+ ? [
67
+ {
68
+ command: qm.command,
69
+ id: generate(),
70
+ delay: 100
71
+ }
72
+ ]
73
+ : []
74
+ )
75
+ for (const q of qms) {
62
76
  const realCmd = isWin
63
- ? qm.command.replace(/\n/g, '\n\r')
64
- : qm.command
77
+ ? q.command.replace(/\n/g, '\n\r')
78
+ : q.command
79
+ await delay(q.delay || 100)
65
80
  runQuickCommand(realCmd, qm.inputOnly)
66
81
  store.editQuickCommand(qm.id, {
67
82
  clickCount: ((qm.clickCount || 0) + 1)
@@ -1,8 +1,8 @@
1
- import { Button, Input, Switch, Form, message, Select } from 'antd'
1
+ import { Button, Switch, Form, message, Select } from 'antd'
2
2
  import copy from 'json-deep-copy'
3
3
  import generate from '../../common/uid'
4
4
  import InputAutoFocus from '../common/input-auto-focus'
5
- const { TextArea } = Input
5
+ import renderQm from './quick-commands-list-form'
6
6
  const FormItem = Form.Item
7
7
  const { Option } = Select
8
8
  const { prefix } = window
@@ -17,13 +17,13 @@ export default function QuickCommandForm (props) {
17
17
  const { formData } = props
18
18
  const {
19
19
  name,
20
- command,
20
+ commands,
21
21
  inputOnly,
22
22
  labels
23
23
  } = res
24
24
  const update = copy({
25
25
  name,
26
- command,
26
+ commands,
27
27
  inputOnly,
28
28
  labels
29
29
  })
@@ -46,6 +46,13 @@ export default function QuickCommandForm (props) {
46
46
  if (!initialValues.labels) {
47
47
  initialValues.labels = []
48
48
  }
49
+ if (!initialValues.commands) {
50
+ initialValues.commands = [{
51
+ command: initialValues.command || '',
52
+ id: generate(),
53
+ delay: 100
54
+ }]
55
+ }
49
56
  return (
50
57
  <Form
51
58
  form={form}
@@ -59,7 +66,7 @@ export default function QuickCommandForm (props) {
59
66
  rules={[{
60
67
  max: 60, message: '60 chars max'
61
68
  }, {
62
- required: true, message: 'name required'
69
+ required: true, message: 'Name required'
63
70
  }]}
64
71
  hasFeedback
65
72
  name='name'
@@ -69,17 +76,7 @@ export default function QuickCommandForm (props) {
69
76
  autofocustrigger={autofocustrigger}
70
77
  />
71
78
  </FormItem>
72
- <FormItem
73
- name='command'
74
- label={t('quickCommand')}
75
- rules={[{
76
- max: 5000, message: '5000 chars max'
77
- }, {
78
- required: true, message: 'command required'
79
- }]}
80
- >
81
- <TextArea rows={3} />
82
- </FormItem>
79
+ {renderQm()}
83
80
  <FormItem
84
81
  name='labels'
85
82
  label={t('label')}
@@ -108,7 +105,7 @@ export default function QuickCommandForm (props) {
108
105
  <FormItem>
109
106
  <p>
110
107
  <Button
111
- type='dashed'
108
+ type='primary'
112
109
  htmlType='submit'
113
110
  >{e('save')}
114
111
  </Button>
@@ -0,0 +1,89 @@
1
+ import {
2
+ Form,
3
+ InputNumber,
4
+ Space,
5
+ Button,
6
+ Input
7
+ } from 'antd'
8
+ import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
9
+ import { formItemLayout } from '../../common/form-layout'
10
+
11
+ const FormItem = Form.Item
12
+ const FormList = Form.List
13
+ const { prefix } = window
14
+ const t = prefix('quickCommands')
15
+
16
+ export default function renderQm () {
17
+ function renderItem (field, i, add, remove) {
18
+ return (
19
+ <Space
20
+ align='center'
21
+ key={field.key}
22
+ >
23
+ <FormItem
24
+ label=''
25
+ name={[field.name, 'delay']}
26
+ required
27
+ >
28
+ <InputNumber
29
+ min={1}
30
+ step={1}
31
+ max={65535}
32
+ addonBefore={t('delay')}
33
+ placeholder={100}
34
+ className='compact-input'
35
+ />
36
+ </FormItem>
37
+ <FormItem
38
+ label=''
39
+ name={[field.name, 'command']}
40
+ required
41
+ className='mg2x'
42
+ >
43
+ <Input.TextArea
44
+ rows={1}
45
+ placeholder={t('quickCommand')}
46
+ className='compact-input'
47
+ />
48
+ </FormItem>
49
+ <Button
50
+ icon={<MinusCircleOutlined />}
51
+ onClick={() => remove(field.name)}
52
+ className='mg24b'
53
+ />
54
+ </Space>
55
+ )
56
+ }
57
+
58
+ return (
59
+ <FormItem {...formItemLayout} label={t('quickCommands')}>
60
+ <FormList
61
+ name='commands'
62
+ >
63
+ {
64
+ (fields, { add, remove }, { errors }) => {
65
+ return (
66
+ <div>
67
+ {
68
+ fields.map((field, i) => {
69
+ return renderItem(field, i, add, remove)
70
+ })
71
+ }
72
+ <FormItem>
73
+ <Button
74
+ type='dashed'
75
+ onClick={() => add()}
76
+ block
77
+ icon={<PlusOutlined />}
78
+ >
79
+ {t('quickCommand')}
80
+ </Button>
81
+ </FormItem>
82
+ </div>
83
+ )
84
+ }
85
+ }
86
+ </FormList>
87
+ </FormItem>
88
+ )
89
+ }
@@ -472,10 +472,11 @@ export default class SessionWrapper extends Component {
472
472
  renderControl = () => {
473
473
  const { splitDirection, terminals, sftpPathFollowSsh } = this.state
474
474
  const { props } = this
475
- const { pane } = props.tab
475
+ const { pane, enableSsh } = props.tab
476
+
476
477
  const termType = props.tab?.type
477
478
  const isSsh = props.tab.authType
478
- const isLocal = termType === connectionMap.local || !termType
479
+ const isLocal = !isSsh && (termType === connectionMap.local || !termType)
479
480
  const isHori = splitDirection === terminalSplitDirectionMap.horizontal
480
481
  const cls1 = 'mg1r icon-split pointer iblock spliter'
481
482
  const cls2 = 'icon-direction pointer iblock spliter'
@@ -542,7 +543,7 @@ export default class SessionWrapper extends Component {
542
543
  }
543
544
  </div>
544
545
  {
545
- isSsh || isLocal
546
+ (isSsh && enableSsh) || isLocal
546
547
  ? (
547
548
  <Tooltip title={checkTxt}>
548
549
  <span {...checkProps}>
@@ -608,7 +609,7 @@ export default class SessionWrapper extends Component {
608
609
  const { pane } = this.props.tab
609
610
  const infoProps = {
610
611
  infoPanelPinned,
611
- ...pick(this.props.config, ['host', 'port', 'saveTerminalLogToFile']),
612
+ ...pick(this.props.config, ['host', 'port', 'saveTerminalLogToFile', 'terminalInfos']),
612
613
  ...infoPanelProps,
613
614
  appPath: this.props.appPath,
614
615
  rightSidebarWidth: this.props.rightSidebarWidth,
@@ -71,6 +71,7 @@ export default function KeywordForm (props) {
71
71
 
72
72
  function renderBefore (name) {
73
73
  const colors = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
74
+ const { themeConfig } = props
74
75
  return (
75
76
  <FormItem name={[name, 'color']} noStyle>
76
77
  <Select>
@@ -79,7 +80,7 @@ export default function KeywordForm (props) {
79
80
  const ps = {
80
81
  className: 'color-picker-choose iblock',
81
82
  style: {
82
- backgroundColor: color
83
+ backgroundColor: themeConfig[color]
83
84
  }
84
85
  }
85
86
  return (
@@ -408,13 +408,15 @@ export default class SettingTerminal extends Component {
408
408
  keywords = [{ color: 'red' }]
409
409
  } = this.props.config
410
410
  const {
411
- appPath
411
+ appPath,
412
+ getThemeConfig
412
413
  } = this.props.store
413
414
  const ps = {
414
415
  formData: {
415
416
  keywords
416
417
  },
417
- submit: this.handleSubmitKeywords
418
+ submit: this.handleSubmitKeywords,
419
+ themeConfig: getThemeConfig()
418
420
  }
419
421
  const terminalLogPath = appPath
420
422
  ? osResolve(appPath, 'electerm', 'session_logs')
@@ -496,6 +498,7 @@ export default class SettingTerminal extends Component {
496
498
  {this.renderToggle('saveTerminalLogToFile', (
497
499
  <ShowItem to={terminalLogPath} className='mg1l'>{p('open')}</ShowItem>
498
500
  ))}
501
+ {this.renderToggle('addTimeStampToTermLog')}
499
502
  {
500
503
  [
501
504
  'cursorBlink',
@@ -45,9 +45,7 @@ export default function SyncForm (props) {
45
45
  if (res.gistId) {
46
46
  up[syncType + 'GistId'] = res.gistId
47
47
  }
48
- if (res.syncPassword) {
49
- up[syncType + 'SyncPassword'] = res.syncPassword
50
- }
48
+ up[syncType + 'SyncPassword'] = res.syncPassword || ''
51
49
  if (res.apiUrl) {
52
50
  up[syncType + 'ApiUrl'] = res.apiUrl
53
51
  }
@@ -761,13 +761,15 @@ export default class FileSection extends React.Component {
761
761
  transferOrEnterDirectory = async (e, edit) => {
762
762
  const { file } = this.state
763
763
  const { isDirectory, type, size } = file
764
+ const isLocal = type === typeMap.local
765
+ const isRemote = type === typeMap.remote
764
766
  if (isDirectory) {
765
767
  return this.enterDirectory(e)
766
768
  }
767
- if (!edit && type === typeMap.local) {
769
+ if (!edit && isLocal) {
768
770
  return this.openFile(this.state.file)
769
771
  }
770
- const remoteEdit = !edit && type === typeMap.remote && size < maxEditFileSize
772
+ const remoteEdit = !edit && isRemote && size < maxEditFileSize
771
773
  if (
772
774
  edit === true || remoteEdit
773
775
  ) {
@@ -787,13 +789,14 @@ export default class FileSection extends React.Component {
787
789
  operation
788
790
  ) => {
789
791
  const { name, path, type, isDirectory } = file
790
- let typeTo = type === typeMap.local
792
+ const isLocal = type === typeMap.local
793
+ let typeTo = isLocal
791
794
  ? typeMap.remote
792
795
  : typeMap.local
793
796
  if (_typeTo) {
794
797
  typeTo = _typeTo
795
798
  }
796
- let toPath = type === typeMap.local
799
+ let toPath = isLocal
797
800
  ? this.props[typeMap.remote + 'Path']
798
801
  : this.props[typeMap.local + 'Path']
799
802
  if (toPathBase) {
@@ -928,10 +931,12 @@ export default class FileSection extends React.Component {
928
931
  } = this.props
929
932
  const hasHost = !!tab.host
930
933
  const { enableSsh } = tab
931
- const transferText = type === typeMap.local
934
+ const isLocal = type === typeMap.local
935
+ const isRemote = type === typeMap.remote
936
+ const transferText = isLocal
932
937
  ? e(transferTypeMap.upload)
933
938
  : e(transferTypeMap.download)
934
- const iconType = type === typeMap.local
939
+ const iconType = isLocal
935
940
  ? 'CloudUploadOutlined'
936
941
  : 'CloudDownloadOutlined'
937
942
  const len = selectedFiles.length
@@ -960,8 +965,8 @@ export default class FileSection extends React.Component {
960
965
  if (
961
966
  isDirectory && id &&
962
967
  (
963
- (hasHost && enableSsh && type === typeMap.remote) ||
964
- (type === typeMap.local && !hasHost)
968
+ (hasHost && enableSsh && isRemote) ||
969
+ (isLocal && !hasHost)
965
970
  )
966
971
  ) {
967
972
  res.push({
@@ -977,14 +982,14 @@ export default class FileSection extends React.Component {
977
982
  text: transferText
978
983
  })
979
984
  }
980
- if (!isDirectory && id && type === typeMap.local) {
985
+ if (!isDirectory && id && isLocal) {
981
986
  res.push({
982
987
  func: 'transferOrEnterDirectory',
983
988
  icon: 'ArrowRightOutlined',
984
989
  text: e('open')
985
990
  })
986
991
  }
987
- if (id && type === typeMap.local) {
992
+ if (id && isLocal) {
988
993
  res.push({
989
994
  func: 'showInDefaultFileManager',
990
995
  icon: 'ContainerOutlined',
@@ -1040,16 +1045,18 @@ export default class FileSection extends React.Component {
1040
1045
  text: m('copyFilePath')
1041
1046
  })
1042
1047
  }
1043
- res.push({
1044
- func: 'newFile',
1045
- icon: 'FileAddOutlined',
1046
- text: e('newFile')
1047
- })
1048
- res.push({
1049
- func: 'newDirectory',
1050
- icon: 'FolderAddOutlined',
1051
- text: e('newFolder')
1052
- })
1048
+ if (enableSsh || isLocal) {
1049
+ res.push({
1050
+ func: 'newFile',
1051
+ icon: 'FileAddOutlined',
1052
+ text: e('newFile')
1053
+ })
1054
+ res.push({
1055
+ func: 'newDirectory',
1056
+ icon: 'FolderAddOutlined',
1057
+ text: e('newFolder')
1058
+ })
1059
+ }
1053
1060
  res.push({
1054
1061
  func: 'selectAll',
1055
1062
  icon: 'CheckSquareOutlined',
@@ -562,6 +562,22 @@ export default class Sftp extends Component {
562
562
  })
563
563
  }
564
564
 
565
+ sftpList = (sftp, remotePath) => {
566
+ return sftp.list(remotePath)
567
+ .then(arr => {
568
+ return arr.map(item => {
569
+ const { type } = item
570
+ return {
571
+ ...pick(item, ['name', 'size', 'accessTime', 'modifyTime', 'mode', 'owner', 'group']),
572
+ isDirectory: type === fileTypeMap.directory,
573
+ type: typeMap.remote,
574
+ path: remotePath,
575
+ id: generate()
576
+ }
577
+ })
578
+ })
579
+ }
580
+
565
581
  remoteList = async (
566
582
  returnList = false,
567
583
  remotePathReal,
@@ -644,52 +660,8 @@ export default class Sftp extends Component {
644
660
  }
645
661
  }
646
662
 
647
- let remote = await sftp.list(remotePath)
663
+ const remote = await this.sftpList(sftp, remotePath)
648
664
  this.sftp = sftp
649
- const remotes = deepCopy(remote)
650
- remote = []
651
- for (const _r of remotes) {
652
- const r = _r
653
- const { type, name } = r
654
- const f = {
655
- ...pick(r, ['name', 'size', 'accessTime', 'modifyTime', 'mode', 'owner', 'group']),
656
- isDirectory: r.type === fileTypeMap.directory,
657
- type: typeMap.remote,
658
- path: remotePath,
659
- id: generate()
660
- }
661
- if (type === fileTypeMap.link) {
662
- const linkPath = resolve(remotePath, name)
663
- let realpath = await sftp.readlink(linkPath)
664
- .catch(e => {
665
- console.debug(e)
666
- return null
667
- })
668
- if (!realpath) {
669
- continue
670
- }
671
- if (!isAbsPath(realpath)) {
672
- realpath = resolve(remotePath, realpath)
673
- realpath = await sftp.realpath(realpath)
674
- }
675
- const realFileInfo = await getRemoteFileInfo(
676
- sftp,
677
- realpath
678
- ).catch(e => {
679
- log.debug('seems a bad symbolic link')
680
- log.debug(e)
681
- return null
682
- })
683
- if (!realFileInfo) {
684
- continue
685
- }
686
- f.isSymbolicLink = true
687
- f.isDirectory = realFileInfo.isDirectory
688
- } else {
689
- f.isSymbolicLink = false
690
- }
691
- remote.push(f)
692
- }
693
665
  const update = {
694
666
  remote,
695
667
  remoteFileTree: buildTree(remote),
@@ -712,6 +684,7 @@ export default class Sftp extends Component {
712
684
  ]).slice(0, maxSftpHistory)
713
685
  }
714
686
  this.setState(update, () => {
687
+ this.updateRemoteList(remote, remotePath, sftp)
715
688
  this.props.editTab(tab.id, {
716
689
  sftpCreated: true
717
690
  })
@@ -731,6 +704,53 @@ export default class Sftp extends Component {
731
704
  }
732
705
  }
733
706
 
707
+ updateRemoteList = async (
708
+ remotes,
709
+ remotePath,
710
+ sftp
711
+ ) => {
712
+ const remote = []
713
+ for (const r of remotes) {
714
+ const { type, name } = r
715
+ if (type === fileTypeMap.link) {
716
+ const linkPath = resolve(remotePath, name)
717
+ let realpath = await sftp.readlink(linkPath)
718
+ .catch(e => {
719
+ console.debug(e)
720
+ return null
721
+ })
722
+ if (!realpath) {
723
+ continue
724
+ }
725
+ if (!isAbsPath(realpath)) {
726
+ realpath = resolve(remotePath, realpath)
727
+ realpath = await sftp.realpath(realpath)
728
+ }
729
+ const realFileInfo = await getRemoteFileInfo(
730
+ sftp,
731
+ realpath
732
+ ).catch(e => {
733
+ log.debug('seems a bad symbolic link')
734
+ log.debug(e)
735
+ return null
736
+ })
737
+ if (!realFileInfo) {
738
+ continue
739
+ }
740
+ r.isSymbolicLink = true
741
+ r.isDirectory = realFileInfo.isDirectory
742
+ } else {
743
+ r.isSymbolicLink = false
744
+ }
745
+ remote.push(r)
746
+ }
747
+ const update = {
748
+ remote,
749
+ remoteFileTree: buildTree(remote)
750
+ }
751
+ this.setState(update)
752
+ }
753
+
734
754
  localList = async (returnList = false, localPathReal, oldPath) => {
735
755
  if (!fs) return
736
756
  if (!returnList) {
@@ -75,7 +75,7 @@ export default class Tabs extends React.Component {
75
75
  const len = tabs.length
76
76
  const addBtnWidth = 22
77
77
  const tabsWidth = this.tabsWidth()
78
- const tabsWidthAll = tabMargin * len + 216 + tabsWidth
78
+ const tabsWidthAll = tabMargin * len + 166 + tabsWidth
79
79
  return width < (tabsWidthAll + addBtnWidth)
80
80
  }
81
81
 
@@ -55,7 +55,7 @@ export default class AttachAddonCustom extends AttachAddon {
55
55
  const data = ev.target.result
56
56
  const { term } = this
57
57
  const str = this.decoder.decode(data)
58
- if (term.parent.props.sftpPathFollowSsh && term.buffer.active.type !== 'alternate') {
58
+ if (term?.parent?.props.sftpPathFollowSsh && term?.buffer.active.type !== 'alternate') {
59
59
  const {
60
60
  cwdId
61
61
  } = term
@@ -78,7 +78,7 @@ export default class AttachAddonCustom extends AttachAddon {
78
78
  }
79
79
  term.write(nnss.join('\r'))
80
80
  } else {
81
- term.write(str)
81
+ term?.write(str)
82
82
  }
83
83
  }
84
84
 
@@ -613,7 +613,7 @@ class Term extends Component {
613
613
  title: 'Choose some files to send',
614
614
  message: 'Choose some files to send',
615
615
  properties
616
- })
616
+ }).catch(() => false)
617
617
  if (!files || !files.length) {
618
618
  return this.onZmodemEnd()
619
619
  }
@@ -637,7 +637,7 @@ class Term extends Component {
637
637
  'treatPackageAsDirectory',
638
638
  'dontAddToRecent'
639
639
  ]
640
- })
640
+ }).catch(() => false)
641
641
  if (!savePaths || !savePaths.length) {
642
642
  return false
643
643
  }
@@ -1203,7 +1203,7 @@ class Term extends Component {
1203
1203
  }
1204
1204
 
1205
1205
  oncloseSocket = () => {
1206
- if (this.onClose) {
1206
+ if (this.onClose || !this.props.tab.enableSsh) {
1207
1207
  return
1208
1208
  }
1209
1209
  this.setStatus(
@@ -1,5 +1,5 @@
1
- import zmodem from 'zmodem.js/src/zmodem_browser'
2
-
1
+ // import zmodem from 'zmodem-ts/dist/zmodem.mjs'
2
+ import Sentry from 'zmodem-ts/dist/zsentry.js'
3
3
  export class AddonZmodem {
4
4
  _disposables = []
5
5
 
@@ -20,7 +20,7 @@ export class AddonZmodem {
20
20
  this.socket = ctx.socket
21
21
  this.term = ctx.term
22
22
  this.ctx = ctx
23
- this.zsentry = new zmodem.Sentry({
23
+ this.zsentry = new Sentry({
24
24
  to_terminal: (octets) => {
25
25
  if (ctx.onZmodem) {
26
26
  this.term.write(String.fromCharCode.apply(String, octets))
@@ -11,8 +11,8 @@ const { prefix } = window
11
11
  const m = prefix('menu')
12
12
 
13
13
  export default function TerminalInfoActivities (props) {
14
- const { activities } = props
15
- if (isEmpty(activities) || !props.isRemote) {
14
+ const { activities, isRemote, terminalInfos } = props
15
+ if (isEmpty(activities) || !isRemote || !terminalInfos.includes('activities')) {
16
16
  return null
17
17
  }
18
18
  const col = colsParser(activities[0])
@@ -7,9 +7,12 @@ import {
7
7
  commonActions
8
8
  } from '../../common/constants'
9
9
  import {
10
- Switch
10
+ Switch,
11
+ Space,
12
+ Button
11
13
  } from 'antd'
12
14
  import ShowItem from '../common/show-item'
15
+ import defaults from '../../common/default-setting'
13
16
  import postMsg from '../../common/post-msg'
14
17
  import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
15
18
 
@@ -54,6 +57,16 @@ export default class TerminalInfoBase extends Component {
54
57
  })
55
58
  }
56
59
 
60
+ toggleTerminalLogInfo = (h) => {
61
+ const { terminalInfos } = this.props
62
+ const nv = terminalInfos.includes(h)
63
+ ? terminalInfos.filter(f => f !== h)
64
+ : [...terminalInfos, h]
65
+ window.store.setConfig({
66
+ terminalInfos: nv
67
+ })
68
+ }
69
+
57
70
  handleToggle = () => {
58
71
  const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
59
72
  const {
@@ -120,10 +133,37 @@ export default class TerminalInfoBase extends Component {
120
133
  unCheckedChildren={name}
121
134
  checked={addTimeStampToTermLog}
122
135
  onChange={this.handleToggleTimestamp}
136
+ className='mg1b'
123
137
  />
124
138
  )
125
139
  }
126
140
 
141
+ renderInfoSelection () {
142
+ const {
143
+ terminalInfos
144
+ } = this.props
145
+ return (
146
+ <Space.Compact block>
147
+ {
148
+ defaults.terminalInfos.map(f => {
149
+ const type = terminalInfos.includes(f) ? 'primary' : 'default'
150
+ return (
151
+ <Button
152
+ key={f + 'term-info-sel'}
153
+ type={type}
154
+ size='small'
155
+ onClick={() => this.toggleTerminalLogInfo(f)}
156
+ className='cap'
157
+ >
158
+ {f}
159
+ </Button>
160
+ )
161
+ })
162
+ }
163
+ </Space.Compact>
164
+ )
165
+ }
166
+
127
167
  render () {
128
168
  const {
129
169
  id,
@@ -149,13 +189,18 @@ export default class TerminalInfoBase extends Component {
149
189
  unCheckedChildren={name}
150
190
  checked={saveTerminalLogToFile}
151
191
  onChange={this.handleToggle}
152
- className='mg1r'
192
+ className='mg1r mg1b'
153
193
  />
154
194
  {
155
195
  this.renderTimestamp()
156
196
  }
157
197
  </span>
158
198
  </div>
199
+ <div className='pd1y'>
200
+ {
201
+ this.renderInfoSelection()
202
+ }
203
+ </div>
159
204
  <p><b>log:</b> {to}</p>
160
205
  </div>
161
206
  )
@@ -7,8 +7,8 @@ import { isEmpty } from 'lodash-es'
7
7
  import colsParser from './data-cols-parser'
8
8
 
9
9
  export default function TerminalInfoDisk (props) {
10
- const { disks } = props
11
- if (isEmpty(disks) || !props.isRemote) {
10
+ const { disks, isRemote, terminalInfos } = props
11
+ if (isEmpty(disks) || !isRemote || !terminalInfos.includes('disks')) {
12
12
  return null
13
13
  }
14
14
  const col = colsParser(disks[0])
@@ -9,8 +9,8 @@ import { formatBytes } from '../../common/byte-format'
9
9
  import copy from 'json-deep-copy'
10
10
 
11
11
  export default function TerminalInfoDisk (props) {
12
- const { network } = props
13
- if (isEmpty(network) || !props.isRemote) {
12
+ const { network, isRemote, terminalInfos } = props
13
+ if (isEmpty(network) || !isRemote || !terminalInfos.includes('network')) {
14
14
  return null
15
15
  }
16
16
  const [state, setter] = useState({
@@ -25,8 +25,13 @@ function computePercent (used, total) {
25
25
  }
26
26
 
27
27
  export default function TerminalInfoResource (props) {
28
- const { cpu, mem, swap } = props
29
- if (!props.isRemote) {
28
+ const { cpu, mem, swap, isRemote, terminalInfos } = props
29
+ if (
30
+ !isRemote ||
31
+ (!terminalInfos.includes('cpu') &&
32
+ !terminalInfos.includes('mem') &&
33
+ !terminalInfos.includes('swap'))
34
+ ) {
30
35
  return null
31
36
  }
32
37
  function renderItem (obj) {
@@ -56,20 +61,25 @@ export default function TerminalInfoResource (props) {
56
61
  </div>
57
62
  )
58
63
  }
59
- const data = [
60
- {
64
+ const data = []
65
+ if (terminalInfos.includes('cpu')) {
66
+ data.push({
61
67
  name: 'cpu',
62
68
  percent: parseInt10(cpu)
63
- },
64
- {
69
+ })
70
+ }
71
+ if (terminalInfos.includes('mem')) {
72
+ data.push({
65
73
  name: 'mem',
66
74
  ...mem
67
- },
68
- {
75
+ })
76
+ }
77
+ if (terminalInfos.includes('swap')) {
78
+ data.push({
69
79
  name: 'swap',
70
80
  ...swap
71
- }
72
- ]
81
+ })
82
+ }
73
83
  return (
74
84
  <div className='terminal-info-section terminal-info-resource'>
75
85
  {
@@ -3,8 +3,8 @@
3
3
  */
4
4
 
5
5
  export default function TerminalInfoUp (props) {
6
- const { uptime } = props
7
- if (!props.isRemote) {
6
+ const { uptime, isRemote, terminalInfos } = props
7
+ if (!isRemote || !terminalInfos.includes('uptime')) {
8
8
  return null
9
9
  }
10
10
  return (
@@ -39,3 +39,6 @@ body
39
39
 
40
40
  a
41
41
  color primary
42
+
43
+ .cap
44
+ text-transform capitalize
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import handleError from '../common/error-handler'
6
+ import { Modal } from 'antd'
6
7
  import { debounce } from 'lodash-es'
7
8
  import postMessage from '../common/post-msg'
8
9
  import {
@@ -15,6 +16,10 @@ import {
15
16
  } from '../common/constants'
16
17
  import * as ls from '../common/safe-local-storage'
17
18
 
19
+ const { prefix } = window
20
+ const m = prefix('menu')
21
+ const c = prefix('common')
22
+
18
23
  export default Store => {
19
24
  Store.prototype.storeAssign = function (updates) {
20
25
  Object.assign(this, updates)
@@ -155,4 +160,50 @@ export default Store => {
155
160
  ls.setItem(dismissDelKeyTipLsKey, 'y')
156
161
  window.store.hideDelKeyTip = true
157
162
  }
163
+ Store.prototype.beforeExit = function (e) {
164
+ const { confirmBeforeExit } = window.store.config
165
+ if (
166
+ (confirmBeforeExit &&
167
+ !window.confirmExit) ||
168
+ window.store.isTransporting
169
+ ) {
170
+ e.returnValue = false
171
+ let mod = null
172
+ mod = Modal.confirm({
173
+ onCancel: () => {
174
+ window.confirmExit = false
175
+ mod.destroy()
176
+ },
177
+ onOk: () => {
178
+ window.confirmExit = true
179
+ window.store[window.exitFunction]()
180
+ },
181
+ title: m('quit'),
182
+ okText: c('ok'),
183
+ cancelText: c('cancel'),
184
+ content: ''
185
+ })
186
+ }
187
+ }
188
+ Store.prototype.beforeExitApp = function (e, name) {
189
+ let mod = null
190
+ mod = Modal.confirm({
191
+ onCancel: () => {
192
+ window.pre.runGlobalAsync('setCloseAction', 'closeApp')
193
+ mod.destroy()
194
+ },
195
+ onOk: () => {
196
+ window.pre.runGlobalAsync(name)
197
+ },
198
+ title: m('quit'),
199
+ okText: c('ok'),
200
+ cancelText: c('cancel'),
201
+ content: ''
202
+ })
203
+ }
204
+ Store.prototype.setTerminalInfos = function (arr) {
205
+ window.store.setConfig({
206
+ terminalInfos: arr
207
+ })
208
+ }
158
209
  }
@@ -91,30 +91,20 @@ export default Store => {
91
91
  }
92
92
 
93
93
  Store.prototype.exit = function () {
94
- const { store } = window
95
- if (
96
- !store.isTransporting &&
97
- !store.config.confirmBeforeExit
98
- ) {
99
- return store.doExit()
100
- }
101
- store.confirmExit()
94
+ window.exitFunction = 'doExit'
95
+ window.store.doExit()
102
96
  }
103
97
 
104
98
  Store.prototype.restart = function () {
105
- const { store } = window
106
- if (store.isTransporting) {
107
- store.confirmExit('doRestart')
108
- } else {
109
- store.doRestart()
110
- }
99
+ window.exitFunction = 'doRestart'
100
+ window.store.doRestart()
111
101
  }
112
102
 
113
103
  Store.prototype.doExit = function () {
114
- window.pre.runGlobalAsync('closeApp')
104
+ window.pre.runGlobalAsync('closeApp', 'exit')
115
105
  }
116
106
 
117
107
  Store.prototype.doRestart = function () {
118
- window.pre.runGlobalAsync('restart')
108
+ window.pre.runGlobalAsync('restart', 'restart')
119
109
  }
120
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.38.11",
3
+ "version": "1.38.30",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",