@electerm/electerm-react 1.38.65 → 1.38.70

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 (35) hide show
  1. package/client/common/constants.js +3 -2
  2. package/client/common/create-title.jsx +9 -1
  3. package/client/common/sftp.js +3 -0
  4. package/client/components/batch-op/batch-op.jsx +1 -6
  5. package/client/components/bookmark-form/bookmark-form.styl +3 -1
  6. package/client/components/bookmark-form/render-ssh-tunnel.jsx +210 -88
  7. package/client/components/bookmark-form/ssh-form-ui.jsx +1 -1
  8. package/client/components/main/main.jsx +14 -0
  9. package/client/components/sftp/{confirm-modal.jsx → confirm-modal-store.jsx} +81 -50
  10. package/client/components/sftp/file-item.jsx +2 -0
  11. package/client/components/sftp/sftp-entry.jsx +27 -37
  12. package/client/components/sftp/transfer-conflict-store.jsx +291 -0
  13. package/client/components/sftp/transport-action-store.jsx +430 -0
  14. package/client/components/sftp/transports-action-store.jsx +102 -0
  15. package/client/components/sftp/transports-ui-store.jsx +30 -0
  16. package/client/components/sidebar/transfer-list-control.jsx +5 -14
  17. package/client/components/sidebar/transport-ui.jsx +2 -12
  18. package/client/components/tabs/tab.jsx +43 -2
  19. package/client/components/tabs/tabs.styl +1 -1
  20. package/client/components/terminal/index.jsx +1 -0
  21. package/client/components/terminal/terminal-interactive.jsx +15 -0
  22. package/client/components/terminal-info/disk.jsx +9 -0
  23. package/client/store/index.js +4 -0
  24. package/client/store/init-state.js +2 -3
  25. package/client/store/sync.js +5 -2
  26. package/client/store/tab.js +1 -1
  27. package/client/store/transfer-list.js +55 -2
  28. package/client/store/watch.js +0 -8
  29. package/package.json +1 -1
  30. package/client/components/sftp/transfer-conflict.jsx +0 -323
  31. package/client/components/sftp/transport-action.jsx +0 -412
  32. package/client/components/sftp/transport-entry.jsx +0 -108
  33. package/client/components/sftp/transport-types.js +0 -8
  34. package/client/components/sftp/transports-action.jsx +0 -111
  35. package/client/components/sftp/transports-ui.jsx +0 -93
@@ -38,7 +38,7 @@ export const contextMenuPaddingTop = 10
38
38
  export const sftpControlHeight = 28 + 42 + 33 + 36
39
39
  export const sidebarWidth = 43
40
40
  export const maxHistory = 50
41
- export const maxTransport = 5
41
+ export const maxTransport = 1
42
42
  export const maxSftpHistory = 20
43
43
  export const maxZoom = 8
44
44
  export const minZoom = 0.5
@@ -278,7 +278,8 @@ export const commonActions = {
278
278
  closeContextMenu: 'close-context-menu',
279
279
  clickContextMenu: 'click-context-menu',
280
280
  openContextMenu: 'open-context-menu',
281
- addTransfer: 'add-transfer'
281
+ addTransfer: 'add-transfer',
282
+ sftpList: 'sftp-list'
282
283
  }
283
284
 
284
285
  export const srcsSkipUpgradeCheck = [
@@ -15,7 +15,7 @@ export default function createTitle (res) {
15
15
  }
16
16
  const {
17
17
  host, port, username, title, type,
18
- path, connectionHoppings
18
+ path, connectionHoppings, sshTunnels
19
19
  } = res
20
20
  const fixTitle = `${username || ''}@${host}:${port}`
21
21
  const extra = host || path ? (path || fixTitle) : ''
@@ -25,6 +25,14 @@ export default function createTitle (res) {
25
25
  if (connectionHoppings && connectionHoppings.length) {
26
26
  f = `[⋙]${f}`
27
27
  }
28
+ if (
29
+ sshTunnels &&
30
+ sshTunnels.length &&
31
+ sshTunnels[0].sshTunnel &&
32
+ sshTunnels[0].sshTunnelRemoteHost
33
+ ) {
34
+ f = `[T]${f}`
35
+ }
28
36
  if (type) {
29
37
  f = `[${type}]${f}`
30
38
  }
@@ -7,6 +7,7 @@ import Transfer from './transfer'
7
7
  import { transferTypeMap, instSftpKeys as keys } from './constants'
8
8
  import initWs from './ws'
9
9
 
10
+ window.sftps = {}
10
11
  const transferKeys = Object.keys(transferTypeMap)
11
12
 
12
13
  class Sftp {
@@ -56,6 +57,7 @@ class Sftp {
56
57
  }
57
58
 
58
59
  async destroy () {
60
+ delete window.sftps[this.sessionId]
59
61
  const { ws } = this
60
62
  ws.s({
61
63
  action: 'sftp-destroy',
@@ -69,5 +71,6 @@ class Sftp {
69
71
  export default async (sessionId) => {
70
72
  const sftp = new Sftp()
71
73
  await sftp.init(sessionId)
74
+ window.sftps[sessionId] = sftp
72
75
  return sftp
73
76
  }
@@ -26,7 +26,6 @@ import { autoRun } from 'manate'
26
26
  import { pick } from 'lodash-es'
27
27
  import { runCmd } from '../terminal/terminal-apis'
28
28
  import deepCopy from 'json-deep-copy'
29
- import postMsg from '../../common/post-msg'
30
29
  import uid from '../../common/uid'
31
30
  import wait from '../../common/wait'
32
31
  import { getFolderFromFilePath } from '../sftp/file-read'
@@ -186,11 +185,7 @@ export default class BatchOp extends Component {
186
185
  zip: true,
187
186
  skipConfirm: true
188
187
  }
189
- postMsg({
190
- list: [obj],
191
- action: commonActions.addTransfer,
192
- sessionId: tab.sessionId
193
- })
188
+ window.store.addTransferList([obj])
194
189
  const { store } = window
195
190
  this.tm = setTimeout(() => {
196
191
  reject(new Error('timeout'))
@@ -5,4 +5,6 @@
5
5
  .mg60b
6
6
  margin-bottom 60px
7
7
  .compact-input input
8
- width 80px !important
8
+ width 80px !important
9
+ .ssh-tunnels-host
10
+ margin-bottom 0
@@ -5,30 +5,154 @@ import {
5
5
  Radio,
6
6
  Space,
7
7
  Button,
8
- Tooltip
8
+ Tooltip,
9
+ Table
9
10
  } from 'antd'
10
- import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
11
- import { formItemLayout } from '../../common/form-layout'
11
+ import { useState } from 'react'
12
+ import { PlusOutlined, QuestionCircleOutlined, MinusCircleFilled } from '@ant-design/icons'
13
+ import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
14
+ import uid from '../../common/uid'
12
15
 
13
16
  const FormItem = Form.Item
14
- const FormList = Form.List
15
17
  const {
16
18
  Button: RadioButton,
17
19
  Group: RadioGroup
18
20
  } = Radio
19
21
  const { prefix } = window
20
22
  const e = prefix('ssh')
23
+ const s = prefix('sftp')
24
+ const m = prefix('menu')
21
25
 
22
- export default function renderSshTunnel () {
23
- function renderItem (field, i, add, remove) {
26
+ export default function renderSshTunnels (props) {
27
+ const {
28
+ form,
29
+ formData
30
+ } = props
31
+ const [formChild] = Form.useForm()
32
+ const [initialValues] = useState({
33
+ sshTunnelLocalPort: 12200,
34
+ sshTunnelLocalHost: '127.0.0.1',
35
+ sshTunnelRemotePort: 12300,
36
+ sshTunnelRemoteHost: '127.0.0.1'
37
+ })
38
+ const [list, setList] = useState(formData.sshTunnels || [])
39
+ function onSubmit () {
40
+ formChild.submit()
41
+ }
42
+ function handleFinish (data) {
43
+ const nd = {
44
+ ...data,
45
+ id: uid()
46
+ }
47
+ const v = [
48
+ ...form.getFieldValue('sshTunnels'),
49
+ nd
50
+ ]
51
+ form.setFieldsValue({
52
+ sshTunnels: v
53
+ })
54
+ setList(old => {
55
+ return [
56
+ ...old,
57
+ data
58
+ ]
59
+ })
60
+ formChild.resetFields()
61
+ }
62
+
63
+ function remove (id) {
64
+ setList(old => {
65
+ return old.filter(i => i.id !== id)
66
+ })
67
+ const v = form.getFieldValue('sshTunnels').filter(i => i.id !== id)
68
+ form.setFieldsValue({
69
+ sshTunnels: v
70
+ })
71
+ formChild.resetFields()
72
+ }
73
+ const cols = [
74
+ {
75
+ title: 'NO.',
76
+ dataIndex: 'index',
77
+ key: 'index',
78
+ render: (k) => k
79
+ }, {
80
+ title: e('sshTunnel'),
81
+ key: 'sshTunnel',
82
+ render: (k, item) => {
83
+ // sshTunnel is forwardRemoteToLocal or forwardLocalToRemote
84
+ const {
85
+ sshTunnel,
86
+ sshTunnelRemoteHost = '127.0.0.1',
87
+ sshTunnelRemotePort,
88
+ sshTunnelLocalHost = '127.0.0.1',
89
+ sshTunnelLocalPort,
90
+ name
91
+ } = item
92
+ const to = sshTunnel === 'forwardRemoteToLocal'
93
+ ? `${s('local')}:${sshTunnelLocalHost}:${sshTunnelLocalPort}`
94
+ : `${s('remote')}:${sshTunnelRemoteHost}:${sshTunnelRemotePort}`
95
+ const from = sshTunnel === 'forwardRemoteToLocal'
96
+ ? `${s('remote')}:${sshTunnelRemoteHost}:${sshTunnelRemotePort}`
97
+ : `${s('local')}:${sshTunnelLocalHost}:${sshTunnelLocalPort}`
98
+ return (
99
+ <span>
100
+ {name ? `[${name}] ` : ''}→ {from} → {to}
101
+ </span>
102
+ )
103
+ }
104
+ }, {
105
+ title: m('del'),
106
+ key: 'op',
107
+ dataIndex: 'id',
108
+ render: (id) => {
109
+ return (
110
+ <MinusCircleFilled
111
+ className='pointer'
112
+ onClick={() => remove(id)}
113
+ />
114
+ )
115
+ }
116
+ }
117
+ ]
118
+
119
+ function renderList () {
24
120
  return (
25
- <Space
26
- align='center'
27
- key={field.key}
121
+ <FormItem {...tailFormItemLayout}>
122
+ <Table
123
+ columns={cols}
124
+ className='mg3b'
125
+ pagination={false}
126
+ size='small'
127
+ dataSource={list.map((d, i) => {
128
+ return {
129
+ ...d,
130
+ index: i + 1
131
+ }
132
+ })}
133
+ />
134
+ </FormItem>
135
+ )
136
+ }
137
+
138
+ return (
139
+ <div>
140
+ <FormItem
141
+ name='sshTunnels'
142
+ className='hide'
143
+ >
144
+ <Input />
145
+ </FormItem>
146
+ <Form
147
+ form={formChild}
148
+ onFinish={handleFinish}
149
+ initialValues={initialValues}
28
150
  >
151
+ {renderList()}
29
152
  <FormItem
30
153
  label={e('sshTunnel')}
31
- name={[field.name, 'sshTunnel']}
154
+ name='sshTunnel'
155
+ {...formItemLayout}
32
156
  required
33
157
  >
34
158
  <RadioGroup>
@@ -48,87 +172,85 @@ export default function renderSshTunnel () {
48
172
  </RadioButton>
49
173
  </RadioGroup>
50
174
  </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>
78
175
  <FormItem
79
- label={e('localPort')}
80
- name={[field.name, 'sshTunnelLocalPort']}
81
- initialValue={22}
176
+ label={s('remote')}
177
+ {...formItemLayout}
82
178
  required
83
- className='mg2x'
179
+ className='ssh-tunnels-host'
84
180
  >
85
- <InputNumber
86
- min={1}
87
- max={65535}
88
- // addonBefore={e('localPort')}
89
- className='compact-input'
90
- placeholder={e('port')}
181
+ <Space.Compact>
182
+ <FormItem
183
+ name='sshTunnelRemoteHost'
184
+ label=''
185
+ required
186
+ >
187
+ <Input
188
+ placeholder={e('host')}
189
+ />
190
+ </FormItem>
191
+ <FormItem
192
+ label=''
193
+ name='sshTunnelRemotePort'
194
+ required
195
+ >
196
+ <InputNumber
197
+ min={1}
198
+ max={65535}
199
+ placeholder={e('port')}
200
+ />
201
+ </FormItem>
202
+ </Space.Compact>
203
+ </FormItem>
204
+ <FormItem
205
+ label={s('local')}
206
+ {...formItemLayout}
207
+ required
208
+ className='ssh-tunnels-host'
209
+ >
210
+ <Space.Compact>
211
+ <FormItem
212
+ name='sshTunnelLocalHost'
213
+ label=''
214
+ required
215
+ >
216
+ <Input
217
+ placeholder={e('host')}
218
+ />
219
+ </FormItem>
220
+ <FormItem
221
+ label=''
222
+ name='sshTunnelLocalPort'
223
+ required
224
+ >
225
+ <InputNumber
226
+ min={1}
227
+ max={65535}
228
+ // addonBefore={e('localPort')}
229
+ placeholder={e('port')}
230
+ />
231
+ </FormItem>
232
+ </Space.Compact>
233
+ </FormItem>
234
+ <FormItem
235
+ name='name'
236
+ label={s('name')}
237
+ {...formItemLayout}
238
+ >
239
+ <Input
240
+ placeholder={e('name')}
91
241
  />
92
242
  </FormItem>
93
- <Button
94
- icon={<MinusCircleOutlined />}
95
- onClick={() => remove(field.name)}
96
- className='mg24b'
97
- />
98
- </Space>
99
- )
100
- }
101
-
102
- return [
103
- <FormList
104
- {...formItemLayout}
105
- label={e('sshTunnel')}
106
- name='sshTunnels'
107
- key='sshTunnels'
108
- >
109
- {
110
- (fields, { add, remove }, { errors }) => {
111
- return (
112
- <div>
113
- {
114
- fields.map((field, i) => {
115
- return renderItem(field, i, add, remove)
116
- })
117
- }
118
- <FormItem>
119
- <Button
120
- type='dashed'
121
- onClick={() => add()}
122
- block
123
- icon={<PlusOutlined />}
124
- >
125
- {e('sshTunnel')}
126
- </Button>
127
- </FormItem>
128
- </div>
129
- )
130
- }
131
- }
132
- </FormList>
133
- ]
243
+ <FormItem {...tailFormItemLayout} className='mg60b'>
244
+ <Button
245
+ type='default'
246
+ htmlType='button'
247
+ icon={<PlusOutlined />}
248
+ onClick={onSubmit}
249
+ >
250
+ {e('sshTunnel')}
251
+ </Button>
252
+ </FormItem>
253
+ </Form>
254
+ </div>
255
+ )
134
256
  }
@@ -65,7 +65,7 @@ export default function BookmarkFormUI (props) {
65
65
  encode: encodes[0],
66
66
  envLang: defaultEnvLang,
67
67
  enableSsh: true,
68
- sshTunnels: [{}],
68
+ sshTunnels: [],
69
69
  runScripts: [{}],
70
70
  category: initBookmarkGroupId,
71
71
  connectionHoppings: []
@@ -12,6 +12,9 @@ import CssOverwrite from './css-overwrite'
12
12
  import UiTheme from './ui-theme'
13
13
  import CustomCss from './custom-css.jsx'
14
14
  import TerminalInteractive from '../terminal/terminal-interactive'
15
+ import ConfirmModalStore from '../sftp/confirm-modal-store.jsx'
16
+ import TransferConflictStore from '../sftp/transfer-conflict-store.jsx'
17
+ import TransportsActionStore from '../sftp/transports-action-store.jsx'
15
18
  import classnames from 'classnames'
16
19
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
17
20
  import { isMac, isWin } from '../../common/constants'
@@ -149,6 +152,17 @@ export default class Index extends Component {
149
152
  />
150
153
  </div>
151
154
  <ContextMenu store={store} />
155
+ <ConfirmModalStore
156
+ store={store}
157
+ />
158
+ <TransferConflictStore
159
+ store={store}
160
+ _fileTransfers={store._fileTransfers}
161
+ />
162
+ <TransportsActionStore
163
+ store={store}
164
+ _fileTransfers={store._fileTransfers}
165
+ />
152
166
  </div>
153
167
  </ConfigProvider>
154
168
  )
@@ -3,6 +3,7 @@
3
3
  *
4
4
  */
5
5
 
6
+ import { Component } from '../common/react-subx'
6
7
  import { Modal, Button } from 'antd'
7
8
  import { isString } from 'lodash-es'
8
9
  import AnimateText from '../common/animate-text'
@@ -23,42 +24,51 @@ function formatTimeAuto (strOrDigit) {
23
24
  return formatTime(strOrDigit * 1000)
24
25
  }
25
26
 
26
- export default (props) => {
27
- if (!props.transferToConfirm) {
28
- return null
29
- }
30
- const {
31
- fromPath,
32
- toPath,
33
- fromFile: {
34
- isDirectory,
35
- name,
36
- id: fileId,
37
- modifyTime: modifyTimeFrom,
38
- size: sizeFrom,
39
- type: typeFrom
40
- },
41
- toFile: {
42
- modifyTime: modifyTimeTo,
43
- size: sizeTo,
44
- type: typeTo
45
- },
46
- id,
47
- transferGroupId
48
- } = props.transferToConfirm
49
- function act (action) {
50
- props.modifier({
51
- transferToConfirm: null
52
- })
27
+ export default class ConfirmModalStore extends Component {
28
+ act (action) {
29
+ const { store } = this.props
30
+ const {
31
+ transferToConfirm
32
+ } = store
33
+ store.setState(
34
+ 'transferToConfirm', {}
35
+ )
36
+ const {
37
+ fromFile: {
38
+ id: fileId
39
+ },
40
+ id,
41
+ transferGroupId
42
+ } = transferToConfirm
53
43
  postMessage({
54
44
  transferGroupId,
55
45
  fileId,
56
46
  id,
57
- transfer: props.transferToConfirm,
47
+ transfer: transferToConfirm,
58
48
  action
59
49
  })
60
50
  }
61
- function renderContent () {
51
+
52
+ renderContent () {
53
+ const {
54
+ transferToConfirm
55
+ } = this.props.store
56
+ const {
57
+ fromPath,
58
+ toPath,
59
+ fromFile: {
60
+ isDirectory,
61
+ name,
62
+ modifyTime: modifyTimeFrom,
63
+ size: sizeFrom,
64
+ type: typeFrom
65
+ },
66
+ toFile: {
67
+ modifyTime: modifyTimeTo,
68
+ size: sizeTo,
69
+ type: typeTo
70
+ }
71
+ } = transferToConfirm
62
72
  const action = isDirectory ? e('merge') : e('replace')
63
73
  const typeTxt = isDirectory ? e('folder') : e('file')
64
74
  const Icon = isDirectory ? FolderOutlined : FileOutlined
@@ -95,27 +105,39 @@ export default (props) => {
95
105
  </div>
96
106
  )
97
107
  }
98
- function renderFooter () {
108
+
109
+ renderFooter () {
110
+ const {
111
+ transferToConfirm
112
+ } = this.props.store
113
+ if (!transferToConfirm) {
114
+ return null
115
+ }
116
+ const {
117
+ fromFile: {
118
+ isDirectory
119
+ }
120
+ } = transferToConfirm
99
121
  return (
100
122
  <div className='mgq1t pd1y alignright'>
101
123
  <Button
102
124
  type='dashed'
103
125
  className='mg1l'
104
- onClick={() => act(fileActions.cancel)}
126
+ onClick={() => this.act(fileActions.cancel)}
105
127
  >
106
128
  {e('cancel')}
107
129
  </Button>
108
130
  <Button
109
131
  type='dashed'
110
132
  className='mg1l'
111
- onClick={() => act(fileActions.skip)}
133
+ onClick={() => this.act(fileActions.skip)}
112
134
  >
113
135
  {e('skip')}
114
136
  </Button>
115
137
  <Button
116
138
  type='dashed'
117
139
  className='mg1l'
118
- onClick={() => act(fileActions.skipAll)}
140
+ onClick={() => this.act(fileActions.skipAll)}
119
141
  >
120
142
  {e('skipAll')}
121
143
  </Button>
@@ -123,7 +145,7 @@ export default (props) => {
123
145
  danger
124
146
  className='mg1l'
125
147
  onClick={
126
- () => act(fileActions.mergeOrOverwrite)
148
+ () => this.act(fileActions.mergeOrOverwrite)
127
149
  }
128
150
  >
129
151
  {isDirectory ? e('merge') : e('overwrite')}
@@ -132,7 +154,7 @@ export default (props) => {
132
154
  type='primary'
133
155
  className='mg1l'
134
156
  onClick={
135
- () => act(fileActions.rename)
157
+ () => this.act(fileActions.rename)
136
158
  }
137
159
  >
138
160
  {e('rename')}
@@ -148,7 +170,7 @@ export default (props) => {
148
170
  : e('overwriteDesc')
149
171
  }
150
172
  onClick={
151
- () => act(fileActions.mergeOrOverwriteAll)
173
+ () => this.act(fileActions.mergeOrOverwriteAll)
152
174
  }
153
175
  >
154
176
  {isDirectory ? e('mergeAll') : e('overwriteAll')}
@@ -158,7 +180,7 @@ export default (props) => {
158
180
  className='mg1l'
159
181
  title={e('renameDesc')}
160
182
  onClick={
161
- () => act(fileActions.renameAll)
183
+ () => this.act(fileActions.renameAll)
162
184
  }
163
185
  >
164
186
  {e('renameAll')}
@@ -167,18 +189,27 @@ export default (props) => {
167
189
  </div>
168
190
  )
169
191
  }
170
- const modalProps = {
171
- open: true,
172
- width: 500,
173
- title: e('fileConflict'),
174
- footer: renderFooter(),
175
- onCancel: () => act(fileActions.cancel)
192
+
193
+ render () {
194
+ const {
195
+ transferToConfirm
196
+ } = this.props.store
197
+ if (!transferToConfirm.id) {
198
+ return null
199
+ }
200
+ const modalProps = {
201
+ open: true,
202
+ width: 500,
203
+ title: e('fileConflict'),
204
+ footer: this.renderFooter(),
205
+ onCancel: () => this.act(fileActions.cancel)
206
+ }
207
+ return (
208
+ <Modal
209
+ {...modalProps}
210
+ >
211
+ {this.renderContent()}
212
+ </Modal>
213
+ )
176
214
  }
177
- return (
178
- <Modal
179
- {...modalProps}
180
- >
181
- {renderContent()}
182
- </Modal>
183
- )
184
215
  }
@@ -168,6 +168,7 @@ export default class FileSection extends React.Component {
168
168
  fromPath,
169
169
  toPath,
170
170
  id: generate(),
171
+ host: this.props.tab?.host,
171
172
  ...createTransferProps(this.props),
172
173
  operation
173
174
  })
@@ -804,6 +805,7 @@ export default class FileSection extends React.Component {
804
805
  }
805
806
  toPath = resolve(toPath, name)
806
807
  const obj = {
808
+ host: this.props.tab?.host,
807
809
  typeFrom: type,
808
810
  typeTo,
809
811
  fromPath: resolve(path, name),