@electerm/electerm-react 3.1.26 → 3.2.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 (29) hide show
  1. package/client/common/db.js +4 -2
  2. package/client/components/ai/ai-history.jsx +4 -4
  3. package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
  4. package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
  5. package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
  6. package/client/components/quick-commands/qm.styl +0 -2
  7. package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
  8. package/client/components/setting-panel/hotkey.jsx +9 -1
  9. package/client/components/setting-panel/list.jsx +0 -1
  10. package/client/components/setting-panel/list.styl +4 -0
  11. package/client/components/setting-panel/setting-modal.jsx +53 -47
  12. package/client/components/shortcuts/shortcut-editor.jsx +4 -2
  13. package/client/components/sidebar/history.jsx +1 -0
  14. package/client/components/terminal/attach-addon-custom.js +86 -0
  15. package/client/components/terminal/cmd-item.jsx +13 -3
  16. package/client/components/terminal/drop-file-modal.jsx +57 -0
  17. package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
  18. package/client/components/terminal/terminal.jsx +103 -5
  19. package/client/components/terminal/terminal.styl +9 -0
  20. package/client/components/tree-list/tree-list-item.jsx +0 -1
  21. package/client/components/vnc/vnc-session.jsx +2 -0
  22. package/client/components/widgets/widget-control.jsx +3 -0
  23. package/client/components/widgets/widget-instance.jsx +26 -7
  24. package/client/css/includes/box.styl +3 -0
  25. package/client/store/init-state.js +2 -1
  26. package/client/store/load-data.js +3 -1
  27. package/client/store/mcp-handler.js +18 -0
  28. package/client/store/widgets.js +54 -0
  29. package/package.json +1 -1
@@ -31,7 +31,8 @@ export const dbNames = [
31
31
  ),
32
32
  'history',
33
33
  'terminalCommandHistory',
34
- 'aiChatHistory'
34
+ 'aiChatHistory',
35
+ 'autoRunWidgets'
35
36
  ]
36
37
  export const dbNamesForSync = [
37
38
  ...without(
@@ -45,7 +46,8 @@ export const dbNamesForWatch = [
45
46
  ...dbNamesForSync,
46
47
  'history',
47
48
  'terminalCommandHistory',
48
- 'aiChatHistory'
49
+ 'aiChatHistory',
50
+ 'autoRunWidgets'
49
51
  ]
50
52
 
51
53
  /**
@@ -4,14 +4,14 @@
4
4
  import { useState, useEffect } from 'react'
5
5
  import { Space } from 'antd'
6
6
  import { HistoryOutlined } from '@ant-design/icons'
7
- import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
7
+ import { safeGetItemJSON, safeSetItemJSON } from '../../common/safe-local-storage'
8
8
  import AiHistoryItem from './ai-history-item'
9
9
 
10
10
  const MAX_HISTORY = 20
11
11
  const e = window.translate
12
12
 
13
13
  export function getHistory (storageKey) {
14
- return getItemJSON(storageKey, [])
14
+ return safeGetItemJSON(storageKey, [])
15
15
  }
16
16
 
17
17
  export function addHistoryItem (storageKey, itemData, eventName) {
@@ -37,7 +37,7 @@ export function addHistoryItem (storageKey, itemData, eventName) {
37
37
  if (history.length > MAX_HISTORY) {
38
38
  history = history.slice(0, MAX_HISTORY)
39
39
  }
40
- setItemJSON(storageKey, history)
40
+ safeSetItemJSON(storageKey, history)
41
41
 
42
42
  // Custom event to trigger update
43
43
  if (eventName) {
@@ -72,7 +72,7 @@ export default function AiHistory (props) {
72
72
  return hStr !== itemStr
73
73
  })
74
74
  setHistory(newHistory)
75
- setItemJSON(storageKey, newHistory)
75
+ safeSetItemJSON(storageKey, newHistory)
76
76
  }
77
77
 
78
78
  if (!history.length) {
@@ -4,6 +4,16 @@ import { TreeSelect } from 'antd'
4
4
 
5
5
  const e = window.translate
6
6
 
7
+ const hoppingProps = [
8
+ 'host',
9
+ 'port',
10
+ 'username',
11
+ 'password',
12
+ 'privateKey',
13
+ 'passphrase',
14
+ 'certificate'
15
+ ]
16
+
7
17
  function buildTreeData (bookmarkGroups, tree) {
8
18
  const cats = bookmarkGroups
9
19
  const btree = cats.reduce((p, k) => ({ ...p, [k.id]: k }), {})
@@ -53,8 +63,14 @@ export default function BookmarkSelect (props) {
53
63
  function onSelect (id) {
54
64
  const item = tree[id]
55
65
  if (item) {
56
- item.bookmarkId = item.id
57
- props.onSelect(item)
66
+ const selected = hoppingProps.reduce((p, k) => {
67
+ if (item[k] !== undefined) {
68
+ p[k] = item[k]
69
+ }
70
+ return p
71
+ }, {})
72
+ selected.bookmarkId = item.id
73
+ props.onSelect(selected)
58
74
  }
59
75
  }
60
76
 
@@ -0,0 +1,153 @@
1
+ import {
2
+ Form,
3
+ InputNumber,
4
+ Input,
5
+ Radio,
6
+ Button
7
+ } from 'antd'
8
+ import {
9
+ formItemLayout,
10
+ tailFormItemLayout
11
+ } from '../../../common/form-layout'
12
+ import {
13
+ PlusOutlined,
14
+ SaveOutlined
15
+ } from '@ant-design/icons'
16
+ import RenderAuth from './render-auth-ssh'
17
+ import {
18
+ authTypeMap
19
+ } from '../../../common/constants'
20
+ import { useState } from 'react'
21
+ import BookmarkSelect from './bookmark-select'
22
+
23
+ const FormItem = Form.Item
24
+ const RadioButton = Radio.Button
25
+ const RadioGroup = Radio.Group
26
+ const e = window.translate
27
+
28
+ export default function ConnectionHoppingForm (props) {
29
+ const {
30
+ store,
31
+ formChild,
32
+ initialValues,
33
+ onFinish,
34
+ authTypes: authTypesProp,
35
+ trim,
36
+ isEdit
37
+ } = props
38
+ const [authType, setAuthType] = useState(initialValues.authType || authTypeMap.password)
39
+
40
+ function onChangeAuthType (e) {
41
+ setAuthType(e.target.value)
42
+ }
43
+
44
+ function onSubmit () {
45
+ formChild.submit()
46
+ }
47
+
48
+ const authTypes = authTypesProp || Object.keys(authTypeMap).map(k => k)
49
+
50
+ const treeProps = {
51
+ bookmarks: store.bookmarks.filter(d => {
52
+ return d.host && d.port && d.username
53
+ }),
54
+ bookmarkGroups: store.bookmarkGroups,
55
+ onSelect: onFinish
56
+ }
57
+
58
+ return (
59
+ <Form
60
+ form={formChild}
61
+ onFinish={onFinish}
62
+ initialValues={initialValues}
63
+ component='div'
64
+ >
65
+ <FormItem
66
+ {...formItemLayout}
67
+ label={e('chooseFromBookmarks')}
68
+ className='mg60b'
69
+ style={{ display: isEdit ? 'none' : '' }}
70
+ >
71
+ <BookmarkSelect {...treeProps} />
72
+ </FormItem>
73
+ <FormItem
74
+ {...formItemLayout}
75
+ label={e('host')}
76
+ hasFeedback
77
+ rules={[{
78
+ max: 520, message: '520 chars max'
79
+ }, {
80
+ required: true, message: 'host required'
81
+ }]}
82
+ normalize={trim}
83
+ name='host'
84
+ >
85
+ <Input />
86
+ </FormItem>
87
+ <FormItem
88
+ {...formItemLayout}
89
+ label={e('port')}
90
+ hasFeedback
91
+ name='port'
92
+ rules={[{
93
+ required: true, message: 'port required'
94
+ }]}
95
+ >
96
+ <InputNumber
97
+ placeholder={e('port')}
98
+ min={1}
99
+ max={65535}
100
+ step={1}
101
+ />
102
+ </FormItem>
103
+ <FormItem
104
+ {...formItemLayout}
105
+ label={e('username')}
106
+ hasFeedback
107
+ name='username'
108
+ rules={[{
109
+ max: 128, message: '128 chars max'
110
+ }]}
111
+ normalize={trim}
112
+ >
113
+ <Input />
114
+ </FormItem>
115
+ <FormItem
116
+ {...tailFormItemLayout}
117
+ className='mg1b'
118
+ name='authType'
119
+ >
120
+ <RadioGroup
121
+ size='small'
122
+ onChange={onChangeAuthType}
123
+ buttonStyle='solid'
124
+ >
125
+ {
126
+ authTypes.map(t => {
127
+ return (
128
+ <RadioButton value={t} key={t}>
129
+ {e(t)}
130
+ </RadioButton>
131
+ )
132
+ })
133
+ }
134
+ </RadioGroup>
135
+ </FormItem>
136
+ <RenderAuth
137
+ form={formChild}
138
+ store={store}
139
+ authType={authType}
140
+ />
141
+ <FormItem {...tailFormItemLayout} className='mg60b'>
142
+ <Button
143
+ type='default'
144
+ htmlType='button'
145
+ icon={isEdit ? <SaveOutlined /> : <PlusOutlined />}
146
+ onClick={onSubmit}
147
+ >
148
+ {isEdit ? e('save') : e('connectionHopping')}
149
+ </Button>
150
+ </FormItem>
151
+ </Form>
152
+ )
153
+ }
@@ -1,33 +1,28 @@
1
1
  import {
2
2
  Form,
3
- InputNumber,
4
3
  Input,
5
- Radio,
6
- Button,
7
4
  Table
8
5
  } from 'antd'
9
6
  import {
10
- formItemLayout,
11
7
  tailFormItemLayout
12
8
  } from '../../../common/form-layout'
13
9
  import {
14
10
  MinusCircleFilled,
15
- PlusOutlined
11
+ EditOutlined,
12
+ HolderOutlined
16
13
  } from '@ant-design/icons'
17
- import RenderAuth from './render-auth-ssh'
18
14
  import uid from '../../../common/uid'
19
15
  import {
20
16
  authTypeMap,
21
17
  connectionHoppingWarnKey
22
18
  } from '../../../common/constants'
23
- import { useState } from 'react'
19
+ import { useState, useRef, useCallback } from 'react'
24
20
  import ConnectionHoppingWarningText from '../../common/connection-hopping-warning-text'
25
- import BookmarkSelect from './bookmark-select'
26
21
  import * as ls from '../../../common/safe-local-storage'
22
+ import Modal from '../../common/modal'
23
+ import ConnectionHoppingForm from './connection-hopping-form'
27
24
 
28
25
  const FormItem = Form.Item
29
- const RadioButton = Radio.Button
30
- const RadioGroup = Radio.Group
31
26
  const e = window.translate
32
27
 
33
28
  export default function renderConnectionHopping (props) {
@@ -37,28 +32,54 @@ export default function renderConnectionHopping (props) {
37
32
  formData
38
33
  } = props
39
34
  const [formChild] = Form.useForm()
40
- const [initialValues, editState] = useState({
35
+ const [editFormChild] = Form.useForm()
36
+ const [initialValues] = useState({
41
37
  port: 22,
42
38
  authType: authTypeMap.password
43
39
  })
44
40
  const [showWarn, setShowWarn] = useState(
45
41
  window.store.hasOldConnectionHoppingBookmark && ls.getItem(connectionHoppingWarnKey) !== 'yes'
46
42
  )
43
+ const [editModalVisible, setEditModalVisible] = useState(false)
44
+ const [editingItem, setEditingItem] = useState(null)
45
+
47
46
  function closeWarn () {
48
47
  setShowWarn(false)
49
48
  }
50
49
  const [list, setList] = useState(formData.connectionHoppings || [])
51
- function onChangeAuthType (e) {
52
- editState(old => {
53
- return {
54
- ...old,
55
- authType: e.target.value
56
- }
50
+ const dragItem = useRef(null)
51
+ const dragOverItem = useRef(null)
52
+
53
+ const handleDragStart = useCallback((index) => {
54
+ dragItem.current = index
55
+ }, [])
56
+
57
+ const handleDragEnter = useCallback((index) => {
58
+ dragOverItem.current = index
59
+ }, [])
60
+
61
+ const handleDragEnd = useCallback(() => {
62
+ if (dragItem.current === null || dragOverItem.current === null) {
63
+ return
64
+ }
65
+ if (dragItem.current === dragOverItem.current) {
66
+ dragItem.current = null
67
+ dragOverItem.current = null
68
+ return
69
+ }
70
+ setList(old => {
71
+ const newList = [...old]
72
+ const [removed] = newList.splice(dragItem.current, 1)
73
+ newList.splice(dragOverItem.current, 0, removed)
74
+ form.setFieldsValue({
75
+ connectionHoppings: newList
76
+ })
77
+ dragItem.current = null
78
+ dragOverItem.current = null
79
+ return newList
57
80
  })
58
- }
59
- function onSubmit () {
60
- formChild.submit()
61
- }
81
+ }, [form])
82
+
62
83
  function handleFinish (data) {
63
84
  const nd = {
64
85
  ...data,
@@ -74,14 +95,11 @@ export default function renderConnectionHopping (props) {
74
95
  setList(old => {
75
96
  return [
76
97
  ...old,
77
- data
98
+ nd
78
99
  ]
79
100
  })
80
101
  formChild.resetFields()
81
102
  }
82
- const authTypes = props.authTypes || Object.keys(authTypeMap).map(k => {
83
- return k
84
- })
85
103
 
86
104
  function remove (id) {
87
105
  setList(old => {
@@ -93,7 +111,51 @@ export default function renderConnectionHopping (props) {
93
111
  })
94
112
  formChild.resetFields()
95
113
  }
114
+
115
+ function openEdit (record) {
116
+ setEditingItem(record)
117
+ setEditModalVisible(true)
118
+ setTimeout(() => {
119
+ editFormChild.setFieldsValue(record)
120
+ }, 100)
121
+ }
122
+
123
+ function handleEditFinish (data) {
124
+ const updatedItem = {
125
+ ...data,
126
+ id: editingItem.id
127
+ }
128
+ setList(old => {
129
+ return old.map(item => item.id === editingItem.id ? updatedItem : item)
130
+ })
131
+ const v = (form.getFieldValue('connectionHoppings') || []).map(
132
+ item => item.id === editingItem.id ? updatedItem : item
133
+ )
134
+ form.setFieldsValue({
135
+ connectionHoppings: v
136
+ })
137
+ setEditModalVisible(false)
138
+ setEditingItem(null)
139
+ editFormChild.resetFields()
140
+ }
141
+
142
+ function closeEditModal () {
143
+ setEditModalVisible(false)
144
+ setEditingItem(null)
145
+ editFormChild.resetFields()
146
+ }
147
+
96
148
  const cols = [
149
+ {
150
+ title: '',
151
+ key: 'drag',
152
+ width: 30,
153
+ render: () => (
154
+ <HolderOutlined
155
+ className='drag'
156
+ />
157
+ )
158
+ },
97
159
  {
98
160
  title: 'NO.',
99
161
  dataIndex: 'index',
@@ -110,15 +172,21 @@ export default function renderConnectionHopping (props) {
110
172
  return <span>{useProfile}{item.username}{pass}@{item.host}:{item.port}{pk}{ph}</span>
111
173
  }
112
174
  }, {
113
- title: e('del'),
175
+ title: e('op'),
114
176
  key: 'op',
115
177
  dataIndex: 'id',
116
- render: (id) => {
178
+ render: (id, record) => {
117
179
  return (
118
- <MinusCircleFilled
119
- className='pointer'
120
- onClick={() => remove(id)}
121
- />
180
+ <span>
181
+ <EditOutlined
182
+ className='pointer mg1r'
183
+ onClick={() => openEdit(record)}
184
+ />
185
+ <MinusCircleFilled
186
+ className='pointer'
187
+ onClick={() => remove(id)}
188
+ />
189
+ </span>
122
190
  )
123
191
  }
124
192
  }
@@ -140,6 +208,13 @@ export default function renderConnectionHopping (props) {
140
208
  className='mg3b'
141
209
  pagination={false}
142
210
  size='small'
211
+ onRow={(record, index) => ({
212
+ draggable: true,
213
+ onDragStart: () => handleDragStart(index),
214
+ onDragEnter: () => handleDragEnter(index),
215
+ onDragEnd: handleDragEnd,
216
+ onDragOver: (e) => e.preventDefault()
217
+ })}
143
218
  dataSource={list.map((d, i) => {
144
219
  return {
145
220
  ...d,
@@ -161,13 +236,15 @@ export default function renderConnectionHopping (props) {
161
236
  </FormItem>
162
237
  )
163
238
  }
164
- const treeProps = {
165
- bookmarks: store.bookmarks.filter(d => {
166
- return d.host && d.port && d.username
167
- }),
168
- bookmarkGroups: store.bookmarkGroups,
169
- onSelect: handleFinish
239
+
240
+ const editModalProps = {
241
+ open: editModalVisible,
242
+ onCancel: closeEditModal,
243
+ footer: null,
244
+ title: e('edit') + ' ' + e('connectionHopping'),
245
+ width: 600
170
246
  }
247
+
171
248
  return (
172
249
  <>
173
250
  <FormItem
@@ -176,100 +253,30 @@ export default function renderConnectionHopping (props) {
176
253
  >
177
254
  <Input />
178
255
  </FormItem>
179
- <Form
180
- form={formChild}
181
- onFinish={handleFinish}
256
+ {renderList()}
257
+ {renderWarn()}
258
+ <ConnectionHoppingForm
259
+ store={store}
260
+ formChild={formChild}
182
261
  initialValues={initialValues}
183
- component='div'
184
- >
185
- {renderList()}
186
- {renderWarn()}
187
- <FormItem
188
- {...formItemLayout}
189
- label={e('chooseFromBookmarks')}
190
- className='mg60b'
191
- >
192
- <BookmarkSelect {...treeProps} />
193
- </FormItem>
194
- <FormItem
195
- {...formItemLayout}
196
- label={e('host')}
197
- hasFeedback
198
- rules={[{
199
- max: 520, message: '520 chars max'
200
- }, {
201
- required: true, message: 'host required'
202
- }]}
203
- normalize={props.trim}
204
- name='host'
205
- >
206
- <Input />
207
- </FormItem>
208
- <FormItem
209
- {...formItemLayout}
210
- label={e('port')}
211
- hasFeedback
212
- name='port'
213
- rules={[{
214
- required: true, message: 'port required'
215
- }]}
216
- >
217
- <InputNumber
218
- placeholder={e('port')}
219
- min={1}
220
- max={65535}
221
- step={1}
262
+ onFinish={handleFinish}
263
+ authTypes={props.authTypes}
264
+ trim={props.trim}
265
+ />
266
+ {editModalVisible && (
267
+ <Modal {...editModalProps}>
268
+ <ConnectionHoppingForm
269
+ key={editingItem?.id}
270
+ store={store}
271
+ formChild={editFormChild}
272
+ initialValues={editingItem}
273
+ onFinish={handleEditFinish}
274
+ authTypes={props.authTypes}
275
+ trim={props.trim}
276
+ isEdit
222
277
  />
223
- </FormItem>
224
- <FormItem
225
- {...formItemLayout}
226
- label={e('username')}
227
- hasFeedback
228
- name='username'
229
- rules={[{
230
- max: 128, message: '128 chars max'
231
- }]}
232
- normalize={props.trim}
233
- >
234
- <Input />
235
- </FormItem>
236
- <FormItem
237
- {...tailFormItemLayout}
238
- className='mg1b'
239
- name='authType'
240
- >
241
- <RadioGroup
242
- size='small'
243
- onChange={onChangeAuthType}
244
- buttonStyle='solid'
245
- >
246
- {
247
- authTypes.map(t => {
248
- return (
249
- <RadioButton value={t} key={t}>
250
- {e(t)}
251
- </RadioButton>
252
- )
253
- })
254
- }
255
- </RadioGroup>
256
- </FormItem>
257
- <RenderAuth
258
- form={formChild}
259
- store={store}
260
- authType={initialValues.authType}
261
- />
262
- <FormItem {...tailFormItemLayout} className='mg60b'>
263
- <Button
264
- type='default'
265
- htmlType='button'
266
- icon={<PlusOutlined />}
267
- onClick={onSubmit}
268
- >
269
- {e('connectionHopping')}
270
- </Button>
271
- </FormItem>
272
- </Form>
278
+ </Modal>
279
+ )}
273
280
  </>
274
281
  )
275
282
  }
@@ -27,8 +27,6 @@
27
27
  border: 1px dashed var(--primary)
28
28
  .qm-item-dragover
29
29
  border-left 2px solid var(--primary)
30
- .qm-drag-handle
31
- cursor grab
32
30
  .qm-field-dragging
33
31
  opacity 0.4
34
32
  .qm-field-dragover
@@ -76,7 +76,7 @@ export default function renderQm (form) {
76
76
  onDrop={(e) => handleDrop(e, i, form)}
77
77
  onDragEnd={handleDragEnd}
78
78
  >
79
- <HolderOutlined className='mg1r qm-drag-handle' />
79
+ <HolderOutlined className='mg1r drag' />
80
80
 
81
81
  <Space.Addon>{e('delay')}</Space.Addon>
82
82
  <FormItem
@@ -51,6 +51,12 @@ export default class HotkeySetting extends Component {
51
51
  })
52
52
  }
53
53
 
54
+ handleClear = () => {
55
+ return this.props.onSaveConfig({
56
+ hotkey: ''
57
+ })
58
+ }
59
+
54
60
  convertToElectronAccelerator = (shortcut) => {
55
61
  if (!shortcut) return shortcut
56
62
 
@@ -117,7 +123,9 @@ export default class HotkeySetting extends Component {
117
123
  index: 0
118
124
  },
119
125
  updateConfig: this.onChangeHotkey,
120
- keysTaken: this.getKeysTaken(hotkey)
126
+ keysTaken: this.getKeysTaken(hotkey),
127
+ handleClear: this.handleClear,
128
+ renderClear: true
121
129
  }
122
130
 
123
131
  return (
@@ -11,7 +11,6 @@ import { noop } from 'lodash-es'
11
11
  import highlight from '../common/highlight'
12
12
  import { settingSyncId, settingCommonId, staticNewItemTabs } from '../../common/constants'
13
13
  import getInitItem from '../../common/init-setting-item'
14
- import './list.styl'
15
14
 
16
15
  const e = window.translate
17
16
 
@@ -8,6 +8,7 @@
8
8
  .list-item-remove
9
9
  .list-item-bookmark
10
10
  .list-item-duplicate
11
+ .list-item-autorun
11
12
  display none
12
13
  width 24px
13
14
  line-height 35px
@@ -15,6 +16,8 @@
15
16
  position absolute
16
17
  right 0
17
18
  top 0
19
+ .list-item-autorun
20
+ right 24px
18
21
 
19
22
  .list-item-title
20
23
  flex-grow: 1
@@ -37,6 +40,7 @@
37
40
  .list-item-remove
38
41
  .list-item-bookmark
39
42
  .list-item-duplicate
43
+ .list-item-autorun
40
44
  display block
41
45
  .theme-item:hover
42
46
  .list-item-remove