@electerm/electerm-react 2.3.151 → 2.3.166

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 (40) hide show
  1. package/client/common/constants.js +4 -2
  2. package/client/common/db.js +2 -1
  3. package/client/common/init-setting-item.js +7 -0
  4. package/client/components/common/modal.jsx +89 -0
  5. package/client/components/common/modal.styl +77 -0
  6. package/client/components/common/notification-with-details.jsx +34 -0
  7. package/client/components/file-transfer/conflict-resolve.jsx +2 -1
  8. package/client/components/file-transfer/transfer-speed-format.js +6 -0
  9. package/client/components/file-transfer/transfer.jsx +5 -2
  10. package/client/components/file-transfer/transports-action-store.jsx +14 -1
  11. package/client/components/main/main.jsx +2 -0
  12. package/client/components/setting-panel/setting-common.jsx +4 -3
  13. package/client/components/setting-panel/start-session-select.jsx +146 -21
  14. package/client/components/setting-panel/text-bg-modal.jsx +15 -4
  15. package/client/components/sftp/file-info-modal.jsx +2 -1
  16. package/client/components/sftp/file-item.jsx +2 -0
  17. package/client/components/sidebar/info-modal.jsx +53 -34
  18. package/client/components/sidebar/info.styl +0 -7
  19. package/client/components/tabs/index.jsx +6 -58
  20. package/client/components/tabs/layout-menu.jsx +75 -0
  21. package/client/components/tabs/layout-select.jsx +60 -0
  22. package/client/components/tabs/tabs.styl +64 -0
  23. package/client/components/tabs/workspace-save-modal.jsx +117 -0
  24. package/client/components/tabs/workspace-select.jsx +79 -0
  25. package/client/components/terminal/attach-addon-custom.js +7 -1
  26. package/client/components/terminal/terminal-interactive.jsx +2 -1
  27. package/client/components/terminal/terminal.jsx +0 -1
  28. package/client/components/text-editor/text-editor.jsx +2 -1
  29. package/client/components/tree-list/move-item-modal.jsx +2 -1
  30. package/client/components/vnc/vnc-session.jsx +2 -2
  31. package/client/components/widgets/widget-control.jsx +4 -5
  32. package/client/components/widgets/widget-form.jsx +3 -8
  33. package/client/components/widgets/widget-instance.jsx +44 -9
  34. package/client/components/widgets/widget-notification-with-details.jsx +34 -0
  35. package/client/css/basic.styl +3 -1
  36. package/client/store/init-state.js +4 -0
  37. package/client/store/load-data.js +15 -6
  38. package/client/store/store.js +2 -0
  39. package/client/store/workspace.js +108 -0
  40. 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
  /**
@@ -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(
@@ -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
  )
@@ -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
@@ -1,15 +1,27 @@
1
- import { TreeSelect } from 'antd'
2
- import { PureComponent } from 'react'
1
+ import { TreeSelect, Tabs, Select, Empty } from 'antd'
2
+ import { useState } from 'react'
3
3
  import copy from 'json-deep-copy'
4
4
  import { createTitleWithTag } from '../../common/create-title'
5
+ import {
6
+ AppstoreOutlined,
7
+ BookOutlined
8
+ } from '@ant-design/icons'
9
+ import HelpIcon from '../common/help-icon'
5
10
 
6
11
  const e = window.translate
7
12
  const { SHOW_CHILD } = TreeSelect
8
13
 
9
- export default class StartSessionSelect extends PureComponent {
10
- buildData = () => {
11
- const cats = this.props.bookmarkGroups
12
- const tree = this.props.bookmarks
14
+ function BookmarkSelect (props) {
15
+ const {
16
+ bookmarks,
17
+ bookmarkGroups,
18
+ onStartSessions,
19
+ onChangeStartSessions
20
+ } = props
21
+
22
+ const buildData = () => {
23
+ const cats = bookmarkGroups
24
+ const tree = bookmarks
13
25
  .reduce((p, k) => {
14
26
  return {
15
27
  ...p,
@@ -64,28 +76,141 @@ export default class StartSessionSelect extends PureComponent {
64
76
  ...(d.bookmarkIds || []).map(buildLeaf)
65
77
  ].filter(d => d)
66
78
  }
67
- // if (!r.children.length) {
68
- // return ''
69
- // }
70
79
  return r
71
80
  }).filter(d => d)
72
81
  return level1
73
82
  }
74
83
 
75
- render () {
76
- const rProps = {
77
- treeData: this.buildData(),
78
- value: copy(this.props.onStartSessions || []),
79
- onChange: this.props.onChangeStartSessions,
80
- treeCheckable: true,
81
- showCheckedStrategy: SHOW_CHILD,
82
- placeholder: e('pleaseSelect'),
83
- style: {
84
- width: '100%'
85
- }
84
+ // onStartSessions is array for bookmarks
85
+ const value = Array.isArray(onStartSessions) ? onStartSessions : []
86
+
87
+ const rProps = {
88
+ treeData: buildData(),
89
+ value: copy(value),
90
+ onChange: onChangeStartSessions,
91
+ treeCheckable: true,
92
+ showCheckedStrategy: SHOW_CHILD,
93
+ placeholder: e('pleaseSelect'),
94
+ style: {
95
+ width: '100%'
86
96
  }
97
+ }
98
+ return (
99
+ <TreeSelect {...rProps} />
100
+ )
101
+ }
102
+
103
+ function WorkspaceSelect (props) {
104
+ const {
105
+ workspaces,
106
+ onStartSessions,
107
+ onChangeStartSessions
108
+ } = props
109
+
110
+ if (!workspaces.length) {
87
111
  return (
88
- <TreeSelect {...rProps} />
112
+ <Empty
113
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
114
+ description={e('noWorkspaces')}
115
+ />
89
116
  )
90
117
  }
118
+
119
+ // onStartSessions is string for workspace
120
+ const value = typeof onStartSessions === 'string' ? onStartSessions : undefined
121
+
122
+ return (
123
+ <Select
124
+ value={value}
125
+ onChange={onChangeStartSessions}
126
+ placeholder={e('workspaces')}
127
+ style={{ width: '100%' }}
128
+ allowClear
129
+ >
130
+ {workspaces.map(w => (
131
+ <Select.Option key={w.id} value={w.id}>
132
+ {w.name}
133
+ </Select.Option>
134
+ ))}
135
+ </Select>
136
+ )
137
+ }
138
+
139
+ export default function StartSessionSelect (props) {
140
+ const {
141
+ onStartSessions,
142
+ bookmarks,
143
+ bookmarkGroups,
144
+ workspaces,
145
+ onChangeStartSessions
146
+ } = props
147
+
148
+ // Determine initial tab based on what's configured
149
+ // string = workspace, array = bookmarks
150
+ const getInitialTab = () => {
151
+ if (typeof onStartSessions === 'string' && onStartSessions) {
152
+ return 'workspaces'
153
+ }
154
+ return 'bookmarks'
155
+ }
156
+
157
+ const [activeTab, setActiveTab] = useState(getInitialTab)
158
+
159
+ // When switching tabs, clear the value if needed
160
+ const handleTabChange = (key) => {
161
+ setActiveTab(key)
162
+ // Reset to appropriate default when switching
163
+ if (key === 'bookmarks' && typeof onStartSessions === 'string') {
164
+ onChangeStartSessions([])
165
+ } else if (key === 'workspaces' && Array.isArray(onStartSessions)) {
166
+ onChangeStartSessions(undefined)
167
+ }
168
+ }
169
+
170
+ const tabItems = [
171
+ {
172
+ key: 'bookmarks',
173
+ label: (
174
+ <span>
175
+ <BookOutlined /> {e('bookmarks')}
176
+ </span>
177
+ )
178
+ },
179
+ {
180
+ key: 'workspaces',
181
+ label: (
182
+ <span>
183
+ <AppstoreOutlined /> {e('workspaces')}
184
+ <HelpIcon link='https://github.com/electerm/electerm/wiki/Workspace-Feature' />
185
+ </span>
186
+ )
187
+ }
188
+ ]
189
+
190
+ return (
191
+ <div>
192
+ <Tabs
193
+ items={tabItems}
194
+ size='small'
195
+ activeKey={activeTab}
196
+ onChange={handleTabChange}
197
+ />
198
+ {activeTab === 'bookmarks'
199
+ ? (
200
+ <BookmarkSelect
201
+ bookmarks={bookmarks}
202
+ bookmarkGroups={bookmarkGroups}
203
+ onStartSessions={onStartSessions}
204
+ onChangeStartSessions={onChangeStartSessions}
205
+ />
206
+ )
207
+ : (
208
+ <WorkspaceSelect
209
+ workspaces={workspaces}
210
+ onStartSessions={onStartSessions}
211
+ onChangeStartSessions={onChangeStartSessions}
212
+ />
213
+ )}
214
+ </div>
215
+ )
91
216
  }
@@ -1,12 +1,13 @@
1
1
  import React, { useState } from 'react'
2
2
  import {
3
- Modal,
4
3
  Input,
5
4
  InputNumber,
6
5
  Space,
7
6
  Typography,
8
- Select
7
+ Select,
8
+ Button
9
9
  } from 'antd'
10
+ import Modal from '../common/modal'
10
11
  import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
11
12
 
12
13
  const { TextArea } = Input
@@ -47,14 +48,24 @@ export default function TextBgModal ({
47
48
  setFontFamily(initialFontFamily)
48
49
  }
49
50
 
51
+ const footer = (
52
+ <>
53
+ <Button onClick={handleCancel}>
54
+ {e('cancel')}
55
+ </Button>
56
+ <Button type='primary' onClick={handleOk} className='mg1l'>
57
+ {e('ok')}
58
+ </Button>
59
+ </>
60
+ )
61
+
50
62
  return (
51
63
  <Modal
52
64
  title={e('terminalBackgroundText')}
53
65
  open={visible}
54
- onOk={handleOk}
55
66
  onCancel={handleCancel}
56
67
  width={500}
57
- destroyOnHidden
68
+ footer={footer}
58
69
  >
59
70
  <div className='pd1'>
60
71
  <Space direction='vertical' size='large' style={{ width: '100%' }}>
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import React from 'react'
6
- import { Modal, Button } from 'antd'
6
+ import { Button } from 'antd'
7
+ import Modal from '../common/modal'
7
8
  import resolve from '../../common/resolve'
8
9
  import time from '../../common/time'
9
10
  import { update } from 'lodash-es'
@@ -183,6 +183,7 @@ export default class FileSection extends React.Component {
183
183
  toPath,
184
184
  id: generate(),
185
185
  host: this.props.tab?.host,
186
+ tabType: this.props.tab?.type,
186
187
  ...createTransferProps(this.props),
187
188
  operation
188
189
  })
@@ -736,6 +737,7 @@ export default class FileSection extends React.Component {
736
737
  toPath = resolve(toPath, name)
737
738
  const obj = {
738
739
  host: this.props.tab?.host,
740
+ tabType: this.props.tab?.type,
739
741
  typeFrom: type,
740
742
  typeTo,
741
743
  fromPath: resolve(path, name),