@electerm/electerm-react 2.3.151 → 2.3.176

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 (56) hide show
  1. package/client/common/constants.js +4 -2
  2. package/client/common/db.js +2 -1
  3. package/client/common/download.jsx +1 -1
  4. package/client/common/error-handler.jsx +1 -1
  5. package/client/common/fetch.jsx +1 -1
  6. package/client/common/init-setting-item.js +7 -0
  7. package/client/components/common/modal.jsx +89 -0
  8. package/client/components/common/modal.styl +77 -0
  9. package/client/components/common/notification-with-details.jsx +34 -0
  10. package/client/components/file-transfer/conflict-resolve.jsx +2 -1
  11. package/client/components/file-transfer/transfer-speed-format.js +6 -0
  12. package/client/components/file-transfer/transfer.jsx +5 -2
  13. package/client/components/file-transfer/transports-action-store.jsx +14 -1
  14. package/client/components/main/connection-hopping-warnning.jsx +1 -1
  15. package/client/components/main/main.jsx +2 -0
  16. package/client/components/quick-commands/qm.styl +0 -10
  17. package/client/components/quick-commands/quick-command-item.jsx +2 -5
  18. package/client/components/quick-commands/quick-commands-box.jsx +12 -23
  19. package/client/components/setting-panel/setting-common.jsx +4 -3
  20. package/client/components/setting-panel/setting-modal.jsx +2 -1
  21. package/client/components/setting-panel/start-session-select.jsx +146 -21
  22. package/client/components/setting-panel/text-bg-modal.jsx +15 -4
  23. package/client/components/setting-sync/setting-sync-form.jsx +1 -1
  24. package/client/components/sftp/file-info-modal.jsx +2 -1
  25. package/client/components/sftp/file-item.jsx +2 -0
  26. package/client/components/sftp/sftp-entry.jsx +1 -1
  27. package/client/components/sftp/sftp.styl +1 -1
  28. package/client/components/sidebar/info-modal.jsx +53 -34
  29. package/client/components/sidebar/info.styl +0 -7
  30. package/client/components/ssh-config/ssh-config-load-notify.jsx +1 -1
  31. package/client/components/tabs/index.jsx +6 -58
  32. package/client/components/tabs/layout-menu.jsx +75 -0
  33. package/client/components/tabs/layout-select.jsx +60 -0
  34. package/client/components/tabs/tabs.styl +64 -0
  35. package/client/components/tabs/workspace-save-modal.jsx +117 -0
  36. package/client/components/tabs/workspace-select.jsx +79 -0
  37. package/client/components/terminal/attach-addon-custom.js +7 -1
  38. package/client/components/terminal/terminal-interactive.jsx +2 -1
  39. package/client/components/terminal/terminal.jsx +1 -2
  40. package/client/components/text-editor/text-editor.jsx +2 -1
  41. package/client/components/tree-list/move-item-modal.jsx +2 -1
  42. package/client/components/vnc/vnc-session.jsx +2 -2
  43. package/client/components/widgets/widget-control.jsx +12 -6
  44. package/client/components/widgets/widget-form.jsx +16 -18
  45. package/client/components/widgets/widget-instance.jsx +44 -9
  46. package/client/components/widgets/widget-notification-with-details.jsx +34 -0
  47. package/client/css/basic.styl +3 -1
  48. package/client/css/includes/box.styl +2 -2
  49. package/client/store/common.js +9 -5
  50. package/client/store/init-state.js +4 -0
  51. package/client/store/load-data.js +15 -6
  52. package/client/store/mcp-handler.js +640 -0
  53. package/client/store/store.js +4 -0
  54. package/client/store/widgets.js +4 -0
  55. package/client/store/workspace.js +108 -0
  56. package/package.json +1 -1
@@ -102,7 +102,8 @@ export const settingMap = buildConst([
102
102
  'quickCommands',
103
103
  'addressBookmarks',
104
104
  'profiles',
105
- 'widgets'
105
+ 'widgets',
106
+ 'workspaces'
106
107
  ])
107
108
 
108
109
  export const staticNewItemTabs = new Set([
@@ -331,7 +332,8 @@ export const syncDataMaps = {
331
332
  terminalThemes: ['terminalThemes'],
332
333
  quickCommands: ['quickCommands'],
333
334
  profiles: ['profiles'],
334
- addressBookmarks: ['addressBookmarks']
335
+ addressBookmarks: ['addressBookmarks'],
336
+ workspaces: ['workspaces']
335
337
  }
336
338
  export const terminalTypes = [
337
339
  'xterm-256color',
@@ -31,7 +31,8 @@ export const dbNames = without(
31
31
 
32
32
  export const dbNamesForWatch = without(
33
33
  Object.keys(settingMap),
34
- settingMap.setting
34
+ settingMap.setting,
35
+ settingMap.widgets
35
36
  )
36
37
 
37
38
  /**
@@ -32,7 +32,7 @@ export default async function download (filename, text) {
32
32
  return
33
33
  }
34
34
  notification.success({
35
- message: '',
35
+ title: '',
36
36
  description: (
37
37
  <ShowItem
38
38
  to={filePath}
@@ -20,7 +20,7 @@ export default (e) => {
20
20
  </div>
21
21
  )
22
22
  notification.error({
23
- message: msg,
23
+ title: msg,
24
24
  description,
25
25
  duration: 55
26
26
  })
@@ -30,7 +30,7 @@ export async function handleErr (res) {
30
30
  }
31
31
  log.debug(text, 'fetch err info')
32
32
  notification.error({
33
- message: 'error',
33
+ title: 'error',
34
34
  description: (
35
35
  <div className='common-err'>
36
36
  {text}
@@ -36,5 +36,12 @@ export default (arr, tab) => {
36
36
  id: '',
37
37
  name: e(settingMap.widgets)
38
38
  }
39
+ } else if (tab === settingMap.workspaces) {
40
+ return {
41
+ id: '',
42
+ name: e(settingMap.workspaces),
43
+ layout: 'default',
44
+ tabsByBatch: {}
45
+ }
39
46
  }
40
47
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Simple modal component without animation
3
+ * Replaces antd Modal for better performance
4
+ */
5
+
6
+ import { CloseOutlined } from '@ant-design/icons'
7
+ import classnames from 'classnames'
8
+ import './modal.styl'
9
+
10
+ export default function Modal (props) {
11
+ const {
12
+ open,
13
+ title,
14
+ width = 520,
15
+ zIndex = 1000,
16
+ className,
17
+ wrapClassName,
18
+ children,
19
+ footer,
20
+ maskClosable = true,
21
+ onCancel
22
+ } = props
23
+
24
+ function handleMaskClick (e) {
25
+ if (e.target === e.currentTarget && maskClosable && onCancel) {
26
+ onCancel()
27
+ }
28
+ }
29
+
30
+ function handleClose () {
31
+ if (onCancel) {
32
+ onCancel()
33
+ }
34
+ }
35
+
36
+ if (!open) {
37
+ return null
38
+ }
39
+
40
+ const modalStyle = {
41
+ zIndex
42
+ }
43
+
44
+ const contentStyle = {
45
+ width: typeof width === 'number' ? `${width}px` : width
46
+ }
47
+
48
+ const cls = classnames(
49
+ 'custom-modal-wrap',
50
+ wrapClassName,
51
+ className
52
+ )
53
+
54
+ return (
55
+ <div className={cls} style={modalStyle}>
56
+ <div
57
+ className='custom-modal-mask'
58
+ onClick={handleMaskClick}
59
+ />
60
+ <div className='custom-modal-container' onClick={handleMaskClick}>
61
+ <div
62
+ className='custom-modal-content'
63
+ style={contentStyle}
64
+ >
65
+ {title && (
66
+ <div className='custom-modal-header'>
67
+ <div className='custom-modal-title'>{title}</div>
68
+ <button
69
+ type='button'
70
+ className='custom-modal-close'
71
+ onClick={handleClose}
72
+ >
73
+ <CloseOutlined />
74
+ </button>
75
+ </div>
76
+ )}
77
+ <div className='custom-modal-body'>
78
+ {children}
79
+ </div>
80
+ {footer !== null && footer !== undefined && (
81
+ <div className='custom-modal-footer'>
82
+ {footer}
83
+ </div>
84
+ )}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,77 @@
1
+ .custom-modal-wrap
2
+ position fixed
3
+ top 0
4
+ left 0
5
+ right 0
6
+ bottom 0
7
+ overflow auto
8
+ outline 0
9
+
10
+ .custom-modal-mask
11
+ position fixed
12
+ top 0
13
+ left 0
14
+ right 0
15
+ bottom 0
16
+ background rgba(0, 0, 0, 0.45)
17
+
18
+ .custom-modal-container
19
+ position relative
20
+ display flex
21
+ align-items flex-start
22
+ justify-content center
23
+ min-height 100%
24
+ padding 24px
25
+
26
+ .custom-modal-content
27
+ position relative
28
+ background var(--main)
29
+ border-radius 8px
30
+ box-shadow 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)
31
+ margin-top 50px
32
+ max-width calc(100vw - 48px)
33
+
34
+ .custom-modal-header
35
+ display flex
36
+ align-items center
37
+ justify-content space-between
38
+ padding 16px 24px
39
+ border-bottom 1px solid var(--main-darker)
40
+
41
+ .custom-modal-title
42
+ font-size 16px
43
+ font-weight 600
44
+ color var(--text)
45
+ line-height 1.5
46
+ flex 1
47
+ overflow hidden
48
+ text-overflow ellipsis
49
+ white-space nowrap
50
+
51
+ .custom-modal-close
52
+ display flex
53
+ align-items center
54
+ justify-content center
55
+ width 32px
56
+ height 32px
57
+ padding 0
58
+ margin-left 8px
59
+ background transparent
60
+ border none
61
+ border-radius 4px
62
+ color var(--text)
63
+ cursor pointer
64
+ transition background-color 0.2s
65
+ &:hover
66
+ background var(--main-darker)
67
+ &:focus
68
+ outline none
69
+
70
+ .custom-modal-body
71
+ padding 0 24px 24px 24px
72
+ color var(--text)
73
+
74
+ .custom-modal-footer
75
+ padding 12px 24px
76
+ border-top 1px solid var(--main-darker)
77
+ text-align right
@@ -0,0 +1,34 @@
1
+ import { notification } from 'antd'
2
+ import { CopyOutlined } from '@ant-design/icons'
3
+ import { copy } from '../../common/clipboard'
4
+
5
+ export function showMsg (message, type = 'success', serverInfo = null, duration = 10, description = '') {
6
+ const handleCopy = () => {
7
+ if (serverInfo && serverInfo.url) {
8
+ copy(serverInfo.url)
9
+ }
10
+ }
11
+
12
+ let desc = description
13
+ if (serverInfo) {
14
+ desc = (
15
+ <div>
16
+ {description && <div>{description}</div>}
17
+ <div style={{ display: 'flex', alignItems: 'center' }}>
18
+ <span>URL: <b>{serverInfo.url}</b></span>
19
+ <CopyOutlined
20
+ className='pointer mg1l'
21
+ onClick={handleCopy}
22
+ />
23
+ </div>
24
+ <div>Path: <b>{serverInfo.path}</b></div>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ notification[type]({
30
+ message,
31
+ description: desc,
32
+ duration
33
+ })
34
+ }
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  import { Component } from 'react'
7
- import { Modal, Button } from 'antd'
7
+ import { Button } from 'antd'
8
+ import Modal from '../common/modal'
8
9
  import { isString } from 'lodash-es'
9
10
  import AnimateText from '../common/animate-text'
10
11
  import formatTime from '../../common/time'
@@ -47,6 +47,12 @@ export const computePassedTime = (startTime) => {
47
47
  }
48
48
 
49
49
  export const computeLeftTime = (bytes, total, startTime) => {
50
+ if (total === 0) {
51
+ return {
52
+ leftTime: '0s',
53
+ leftTimeInt: 0
54
+ }
55
+ }
50
56
  let now = Date.now()
51
57
  if (now <= startTime) {
52
58
  now = startTime + 1
@@ -71,6 +71,9 @@ export default class TransportAction extends Component {
71
71
  this.transport = null
72
72
  this.fromFile = null
73
73
  refsTransfers.remove(this.id)
74
+ if (this.isFtp) {
75
+ window.initingFtpTabIds?.delete(this.tabId)
76
+ }
74
77
  }
75
78
 
76
79
  localCheckExist = (path) => {
@@ -185,9 +188,9 @@ export default class TransportAction extends Component {
185
188
  const up = {}
186
189
  const total = transfer.fromFile.size
187
190
  let percent = total === 0
188
- ? 0
191
+ ? 100
189
192
  : Math.floor(100 * transferred / total)
190
- percent = percent >= 100 ? 99 : percent
193
+ percent = percent >= 100 ? 100 : percent
191
194
  up.percent = percent
192
195
  up.status = 'active'
193
196
  up.transferred = transferred
@@ -9,6 +9,8 @@ import { maxTransport } from '../../common/constants'
9
9
  import { refsStatic } from '../common/ref'
10
10
  // import { action } from 'manate'
11
11
 
12
+ window.initingFtpTabIds = new Set()
13
+
12
14
  export default class TransportsActionStore extends Component {
13
15
  componentDidMount () {
14
16
  this.control()
@@ -71,7 +73,9 @@ export default class TransportsActionStore extends Component {
71
73
  typeTo,
72
74
  typeFrom,
73
75
  inited,
74
- id
76
+ id,
77
+ tabType,
78
+ tabId
75
79
  } = tr
76
80
 
77
81
  const isTransfer = typeTo !== typeFrom
@@ -80,6 +84,15 @@ export default class TransportsActionStore extends Component {
80
84
  continue
81
85
  }
82
86
 
87
+ // For ftp transfers, ensure only one per tabId is inited
88
+ if (tabType === 'ftp') {
89
+ const hasInited = fileTransfers.some(t => t.tabId === tabId && t.inited && t.id !== id)
90
+ if (hasInited || window.initingFtpTabIds.has(tabId)) {
91
+ continue
92
+ }
93
+ window.initingFtpTabIds.add(tabId)
94
+ }
95
+
83
96
  if (count < maxTransport) {
84
97
  count++
85
98
  refsStatic.get('transfer-queue')?.addToQueue(
@@ -27,7 +27,7 @@ export default function ConnectionHoppingWarning (props) {
27
27
  return
28
28
  }
29
29
  notification.info({
30
- message: e('connectionHopping'),
30
+ title: e('connectionHopping'),
31
31
  duration: 0,
32
32
  placement: 'bottom',
33
33
  key: connectionHoppingWarnKey,
@@ -31,6 +31,7 @@ import AIChat from '../ai/ai-chat'
31
31
  import Opacity from '../common/opacity'
32
32
  import MoveItemModal from '../tree-list/move-item-modal'
33
33
  import InputContextMenu from '../common/input-context-menu'
34
+ import WorkspaceSaveModal from '../tabs/workspace-save-modal'
34
35
  import { pick } from 'lodash-es'
35
36
  import deepCopy from 'json-deep-copy'
36
37
  import './wrapper.styl'
@@ -305,6 +306,7 @@ export default auto(function Index (props) {
305
306
  <ConnectionHoppingWarning {...warningProps} />
306
307
  <TerminalCmdSuggestions {...cmdSuggestionsProps} />
307
308
  <TransferQueue />
309
+ <WorkspaceSaveModal store={store} />
308
310
  </div>
309
311
  </ConfigProvider>
310
312
  )
@@ -17,16 +17,6 @@
17
17
  .qm-list-wrap
18
18
  max-height 100px
19
19
 
20
- .fil-keyword
21
- .fil-label
22
- .qm-item
23
- color var(--text-disabled)
24
- .qm-item
25
- &.name-match
26
- &.label-match
27
- font-weight bold
28
- color var(--text)
29
-
30
20
  .qm-search-input
31
21
  max-width 200px
32
22
  @media (max-width: 500px)
@@ -18,17 +18,14 @@ export default class QuickCommandsItem extends PureComponent {
18
18
  }
19
19
 
20
20
  render () {
21
- const { name, id, nameMatch, labelMatch, shortcut } = this.props.item
21
+ const { name, id, shortcut } = this.props.item
22
22
  const {
23
23
  draggable,
24
24
  handleDragOver,
25
25
  handleDragStart,
26
26
  handleDrop
27
27
  } = this.props
28
- const cls = classNames('qm-item mg1r mg1b', {
29
- 'name-match': nameMatch,
30
- 'label-match': labelMatch
31
- })
28
+ const cls = classNames('qm-item mg1r mg1b')
32
29
  const btnProps = {
33
30
  className: cls,
34
31
  onClick: this.handleSelect,
@@ -129,15 +129,12 @@ export default function QuickCommandsFooterBox (props) {
129
129
  )
130
130
  }
131
131
 
132
- function sortArray (array, keyword, labels, qmSortByFrequency) {
133
- const sorters = [
134
- (obj) => !(keyword && obj.name.toLowerCase().includes(keyword)),
135
- (obj) => !labels.some((label) => (obj.labels || []).includes(label))
136
- ]
137
- if (qmSortByFrequency) {
138
- sorters.push((obj) => -(obj.clickCount || 0))
139
- }
140
- return sortBy(array, sorters)
132
+ function filterArray (array, keyword, labels) {
133
+ return array.filter(obj => {
134
+ const nameMatches = !keyword || obj.name.toLowerCase().includes(keyword)
135
+ const labelMatches = !labels.length || labels.some((label) => (obj.labels || []).includes(label))
136
+ return nameMatches && labelMatches
137
+ })
141
138
  }
142
139
 
143
140
  const {
@@ -156,14 +153,10 @@ export default function QuickCommandsFooterBox (props) {
156
153
  return renderNoCmd()
157
154
  }
158
155
  const keyword0 = keyword.toLowerCase()
159
- const filtered = sortArray(all, keyword0, labels, qmSortByFrequency)
160
- .map(d => {
161
- return {
162
- ...d,
163
- nameMatch: keyword && d.name.toLowerCase().includes(keyword),
164
- labelMatch: labels.some((label) => (d.labels || []).includes(label))
165
- }
166
- })
156
+ const filtered = filterArray(all, keyword0, labels)
157
+ const sorted = qmSortByFrequency
158
+ ? sortBy(filtered, (obj) => -(obj.clickCount || 0))
159
+ : filtered
167
160
  const sprops = {
168
161
  value: labels,
169
162
  mode: 'multiple',
@@ -174,11 +167,7 @@ export default function QuickCommandsFooterBox (props) {
174
167
  const tp = pinnedQuickCommandBar
175
168
  ? 'primary'
176
169
  : 'text'
177
- const cls = classNames(
178
- 'qm-list-wrap',
179
- { 'fil-label': !!labels.length },
180
- { 'fil-keyword': !!keyword }
181
- )
170
+ const cls = classNames('qm-list-wrap')
182
171
  const type = qmSortByFrequency ? 'primary' : 'default'
183
172
  const w = openedSideBar ? 43 + leftSidebarWidth : 43
184
173
  const qmProps = {
@@ -233,7 +222,7 @@ export default function QuickCommandsFooterBox (props) {
233
222
  </Space.Compact>
234
223
  </Flex>
235
224
  <div className={cls}>
236
- {filtered.map(renderItem)}
225
+ {sorted.map(renderItem)}
237
226
  </div>
238
227
  </div>
239
228
  </div>
@@ -518,6 +518,7 @@ export default class SettingCommon extends Component {
518
518
  onStartSessions: props.config.onStartSessions,
519
519
  bookmarks: props.bookmarks,
520
520
  bookmarkGroups: props.bookmarkGroups,
521
+ workspaces: props.store.workspaces,
521
522
  onChangeStartSessions: this.onChangeStartSessions
522
523
  }
523
524
  return (
@@ -528,7 +529,7 @@ export default class SettingCommon extends Component {
528
529
  <Select
529
530
  value={modifier}
530
531
  onChange={this.handleChangeModifier}
531
- className='iblock width100'
532
+ className='width100'
532
533
  popupMatchSelectWidth={false}
533
534
  showSearch
534
535
  >
@@ -536,10 +537,10 @@ export default class SettingCommon extends Component {
536
537
  modifiers.map(this.renderOption)
537
538
  }
538
539
  </Select>
539
- <span className='iblock mg1x'>+</span>
540
+ <span className='mg1x'>+</span>
540
541
  <Select
541
542
  value={key}
542
- className='iblock width100'
543
+ className='width100'
543
544
  onChange={this.handleChangeKey}
544
545
  popupMatchSelectWidth={false}
545
546
  showSearch
@@ -39,7 +39,7 @@ export default auto(function SettingModalWrap (props) {
39
39
  shouldConfirmDel: tabsShouldConfirmDel.includes(settingTab),
40
40
  list: settingSidebarList
41
41
  }
42
- const { bookmarks, bookmarkGroups } = store
42
+ const { bookmarks, bookmarkGroups, widgetInstances } = store
43
43
  const formProps = {
44
44
  store,
45
45
  formData: settingItem,
@@ -51,6 +51,7 @@ export default auto(function SettingModalWrap (props) {
51
51
  ]),
52
52
  bookmarkGroups,
53
53
  bookmarks,
54
+ widgetInstancesLength: widgetInstances.length,
54
55
  serials: store.serials,
55
56
  loaddingSerials: store.loaddingSerials
56
57
  }