@electerm/electerm-react 1.38.19 → 1.38.40

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 (34) hide show
  1. package/client/common/build-ssh-tunnel.js +2 -1
  2. package/client/common/choose-save-folder.js +22 -0
  3. package/client/common/constants.js +1 -1
  4. package/client/common/default-setting.js +9 -1
  5. package/client/common/download.jsx +31 -0
  6. package/client/common/trzsz.js +2 -18
  7. package/client/components/bookmark-form/render-ssh-tunnel.jsx +32 -14
  8. package/client/components/main/main.jsx +4 -0
  9. package/client/components/quick-commands/quick-commands-box.jsx +20 -5
  10. package/client/components/quick-commands/quick-commands-form-elem.jsx +14 -17
  11. package/client/components/quick-commands/quick-commands-list-form.jsx +89 -0
  12. package/client/components/session/session.jsx +5 -4
  13. package/client/components/setting-panel/list.styl +3 -0
  14. package/client/components/setting-panel/setting-terminal.jsx +1 -0
  15. package/client/components/setting-panel/tree-list.styl +4 -0
  16. package/client/components/sftp/file-item.jsx +27 -20
  17. package/client/components/sftp/sftp-entry.jsx +65 -45
  18. package/client/components/shortcuts/shortcuts-defaults.js +7 -7
  19. package/client/components/tabs/index.jsx +12 -1
  20. package/client/components/terminal/attach-addon-custom.js +2 -2
  21. package/client/components/terminal/index.jsx +18 -15
  22. package/client/components/terminal-info/activity.jsx +4 -4
  23. package/client/components/terminal-info/base.jsx +58 -2
  24. package/client/components/terminal-info/disk.jsx +4 -3
  25. package/client/components/terminal-info/network.jsx +4 -3
  26. package/client/components/terminal-info/resource.jsx +20 -10
  27. package/client/components/terminal-info/up.jsx +5 -3
  28. package/client/css/basic.styl +3 -0
  29. package/client/store/common.js +51 -0
  30. package/client/store/index.js +2 -2
  31. package/client/store/sync.js +15 -2
  32. package/client/store/system-menu.js +6 -16
  33. package/package.json +1 -1
  34. package/client/common/download.js +0 -16
@@ -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
  }
@@ -0,0 +1,22 @@
1
+ const {
2
+ openDialog
3
+ } = window.api
4
+
5
+ export async function chooseSaveDirectory () {
6
+ const savePaths = await openDialog({
7
+ title: 'Choose a folder to save file(s)',
8
+ message: 'Choose a folder to save file(s)',
9
+ properties: [
10
+ 'openDirectory',
11
+ 'showHiddenFiles',
12
+ 'createDirectory',
13
+ 'noResolveAliases',
14
+ 'treatPackageAsDirectory',
15
+ 'dontAddToRecent'
16
+ ]
17
+ })
18
+ if (!savePaths || !savePaths.length) {
19
+ return undefined
20
+ }
21
+ return savePaths[0]
22
+ }
@@ -20,7 +20,7 @@ const buildConst = (props) => {
20
20
  export const logoPath1 = logoPath1Ref.replace(/^\//, '')
21
21
  export const logoPath2 = logoPath2Ref.replace(/^\//, '')
22
22
  export const logoPath3 = logoPath3Ref.replace(/^\//, '')
23
- export const maxEditFileSize = 1024 * 30
23
+ export const maxEditFileSize = 1024 * 3000
24
24
  export const defaultBookmarkGroupId = 'default'
25
25
  export const newBookmarkIdPrefix = 'new-bookmark'
26
26
  export const unexpectedPacketErrorDesc = 'Unexpected packet'
@@ -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
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * simulate download
3
+ */
4
+ import { notification } from 'antd'
5
+ import ShowItem from '../components/common/show-item'
6
+ import { chooseSaveDirectory } from './choose-save-folder'
7
+
8
+ export default async function download (filename, text) {
9
+ const savePath = await chooseSaveDirectory()
10
+ if (!savePath) {
11
+ return
12
+ }
13
+ const path = window.require('path')
14
+ const filePath = path.join(savePath, filename)
15
+ const r = await window.fs.writeFile(filePath, text).catch(window.store.onError)
16
+ if (!r) {
17
+ return
18
+ }
19
+ notification.success({
20
+ message: '',
21
+ description: (
22
+ <div>
23
+ <ShowItem
24
+ to={filePath}
25
+ >
26
+ {filePath}
27
+ </ShowItem>
28
+ </div>
29
+ )
30
+ })
31
+ }
@@ -1,4 +1,5 @@
1
1
  import { TrzszFilter } from 'trzsz'
2
+ import { chooseSaveDirectory } from './choose-save-folder'
2
3
 
3
4
  const {
4
5
  openDialog
@@ -36,24 +37,7 @@ window.newTrzsz = function (
36
37
  })
37
38
  },
38
39
  // choose a directory to save the received files
39
- chooseSaveDirectory: async () => {
40
- const savePaths = await openDialog({
41
- title: 'Choose a folder to save file(s)',
42
- message: 'Choose a folder to save file(s)',
43
- properties: [
44
- 'openDirectory',
45
- 'showHiddenFiles',
46
- 'createDirectory',
47
- 'noResolveAliases',
48
- 'treatPackageAsDirectory',
49
- 'dontAddToRecent'
50
- ]
51
- })
52
- if (!savePaths || !savePaths.length) {
53
- return undefined
54
- }
55
- return savePaths[0]
56
- },
40
+ chooseSaveDirectory,
57
41
  // the terminal columns
58
42
  terminalColumns,
59
43
  // there is a windows shell
@@ -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,
@@ -10,6 +10,9 @@
10
10
  width 24px
11
11
  line-height 35px
12
12
  text-align center
13
+ position absolute
14
+ right 0
15
+ top 0
13
16
  .list-item-title
14
17
  flex-grow: 1
15
18
  .item-list-unit
@@ -498,6 +498,7 @@ export default class SettingTerminal extends Component {
498
498
  {this.renderToggle('saveTerminalLogToFile', (
499
499
  <ShowItem to={terminalLogPath} className='mg1l'>{p('open')}</ShowItem>
500
500
  ))}
501
+ {this.renderToggle('addTimeStampToTermLog')}
501
502
  {
502
503
  [
503
504
  'cursorBlink',
@@ -9,6 +9,9 @@
9
9
  width 16px
10
10
  line-height 26px
11
11
  height 20px
12
+ position absolute
13
+ right 0
14
+ top 0
12
15
  .tree-item
13
16
  display flex
14
17
  &.is-category
@@ -25,6 +28,7 @@
25
28
  overflow hidden
26
29
  white-space nowrap
27
30
  text-overflow ellipsis
31
+ padding-right 20px
28
32
  .sidebar-panel .tree-item-title
29
33
  max-width 180px
30
34
  .with-plus
@@ -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',