@electerm/electerm-react 2.3.136 → 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 (59) 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/batch-op/batch-op.jsx +3 -8
  5. package/client/components/bookmark-form/common/color-picker.jsx +16 -5
  6. package/client/components/bookmark-form/common/color-picker.styl +1 -2
  7. package/client/components/bookmark-form/common/connection-hopping.jsx +1 -0
  8. package/client/components/bookmark-form/config/common-fields.js +1 -0
  9. package/client/components/common/drawer.jsx +62 -0
  10. package/client/components/common/drawer.styl +34 -0
  11. package/client/components/common/modal.jsx +89 -0
  12. package/client/components/common/modal.styl +77 -0
  13. package/client/components/common/notification-with-details.jsx +34 -0
  14. package/client/components/file-transfer/conflict-resolve.jsx +2 -1
  15. package/client/components/file-transfer/transfer-speed-format.js +6 -0
  16. package/client/components/file-transfer/transfer.jsx +5 -2
  17. package/client/components/file-transfer/transports-action-store.jsx +14 -1
  18. package/client/components/main/main.jsx +2 -0
  19. package/client/components/profile/profile-form.jsx +1 -1
  20. package/client/components/quick-commands/qm.styl +2 -1
  21. package/client/components/quick-commands/quick-commands-form.jsx +1 -1
  22. package/client/components/setting-panel/list.jsx +1 -1
  23. package/client/components/setting-panel/setting-common.jsx +5 -4
  24. package/client/components/setting-panel/setting-terminal.jsx +1 -1
  25. package/client/components/setting-panel/setting-wrap.jsx +4 -10
  26. package/client/components/setting-panel/setting-wrap.styl +8 -6
  27. package/client/components/setting-panel/start-session-select.jsx +146 -21
  28. package/client/components/setting-panel/text-bg-modal.jsx +15 -4
  29. package/client/components/sftp/file-info-modal.jsx +2 -1
  30. package/client/components/sftp/file-item.jsx +2 -0
  31. package/client/components/sftp/paged-list.jsx +2 -1
  32. package/client/components/sftp/sftp-entry.jsx +1 -1
  33. package/client/components/sftp/sftp.styl +13 -0
  34. package/client/components/sidebar/info-modal.jsx +53 -34
  35. package/client/components/sidebar/info.styl +0 -7
  36. package/client/components/tabs/index.jsx +6 -58
  37. package/client/components/tabs/layout-menu.jsx +75 -0
  38. package/client/components/tabs/layout-select.jsx +60 -0
  39. package/client/components/tabs/tabs.styl +64 -0
  40. package/client/components/tabs/workspace-save-modal.jsx +117 -0
  41. package/client/components/tabs/workspace-select.jsx +79 -0
  42. package/client/components/terminal/attach-addon-custom.js +7 -1
  43. package/client/components/terminal/terminal-interactive.jsx +2 -1
  44. package/client/components/terminal/terminal.jsx +0 -1
  45. package/client/components/text-editor/text-editor.jsx +2 -1
  46. package/client/components/tree-list/move-item-modal.jsx +115 -30
  47. package/client/components/tree-list/tree-list.jsx +1 -1
  48. package/client/components/tree-list/tree-list.styl +6 -1
  49. package/client/components/vnc/vnc-session.jsx +2 -2
  50. package/client/components/widgets/widget-control.jsx +4 -5
  51. package/client/components/widgets/widget-form.jsx +3 -8
  52. package/client/components/widgets/widget-instance.jsx +44 -9
  53. package/client/components/widgets/widget-notification-with-details.jsx +34 -0
  54. package/client/css/basic.styl +3 -1
  55. package/client/store/init-state.js +4 -0
  56. package/client/store/load-data.js +15 -6
  57. package/client/store/store.js +2 -0
  58. package/client/store/workspace.js +108 -0
  59. package/package.json +1 -1
@@ -1,18 +1,20 @@
1
1
 
2
- body .ant-drawer .ant-drawer-content-wrapper
3
- left 43px
4
- .close-setting-wrap-icon
2
+
3
+ .close-setting-wrap
5
4
  position absolute
6
5
  top 70px
7
6
  font-size 16px
8
7
  cursor pointer
9
8
  z-index 889
9
+ right 20px
10
10
  &:hover
11
11
  color var(--success)
12
12
  .alt-close-setting-wrap
13
- right 20px
14
- .close-setting-wrap
15
- top 70px
13
+ left 20px
14
+ right auto
15
+ .batch-op-wrap .close-setting-wrap
16
+ top 40px
17
+ right 30px
16
18
  .setting-row
17
19
  position absolute
18
20
  top 127px
@@ -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),
@@ -37,8 +37,9 @@ export default class ScrollFiles extends Component {
37
37
  current: this.state.page,
38
38
  pageSize: this.state.pageSize,
39
39
  total: this.props.list.length,
40
+ showLessItems: true,
40
41
  showSizeChanger: false,
41
- simple: true,
42
+ simple: false,
42
43
  onChange: this.onChange
43
44
  }
44
45
  return (
@@ -63,7 +63,7 @@ export default class Sftp extends Component {
63
63
  this.setState({
64
64
  ready: true
65
65
  })
66
- }, 100)
66
+ }, 0)
67
67
  }
68
68
 
69
69
  componentDidUpdate (prevProps, prevState) {
@@ -164,6 +164,19 @@
164
164
  .pager-wrap
165
165
  z-index 4
166
166
  position relative
167
+
168
+ .sftp-has-pager
169
+ .sftp-table-content
170
+ position static
171
+ padding-bottom 42px
172
+ .pager-wrap
173
+ position absolute
174
+ bottom 0
175
+ left 0
176
+ width 100%
177
+ background var(--main)
178
+ border-top 1px solid var(--main-darker)
179
+
167
180
  .file-header-context-menu
168
181
  position fixed
169
182
  z-index 999
@@ -8,10 +8,10 @@ import {
8
8
  InfoCircleOutlined,
9
9
  AlignLeftOutlined,
10
10
  BugOutlined,
11
- HeartOutlined,
12
- JavaScriptOutlined
11
+ HeartOutlined
13
12
  } from '@ant-design/icons'
14
- import { Modal, Tabs, Button } from 'antd'
13
+ import { Tabs, Button } from 'antd'
14
+ import Modal from '../common/modal'
15
15
  import Link from '../common/external-link'
16
16
  import LogoElem from '../common/logo-elem'
17
17
  import RunningTime from './app-running-time'
@@ -55,8 +55,48 @@ export default auto(function InfoModal (props) {
55
55
  )
56
56
  }
57
57
 
58
- const formatJSON = (jsonStr) => {
59
- return JSON.stringify(JSON.parse(jsonStr), null, 2)
58
+ const renderParsed = (obj, depth = 0) => {
59
+ if (Array.isArray(obj)) {
60
+ return (
61
+ <ul className='pd2l'>
62
+ {obj.map((item, i) => (
63
+ <li key={i}>{renderParsed(item, depth + 1)}</li>
64
+ ))}
65
+ </ul>
66
+ )
67
+ } else if (typeof obj === 'object' && obj !== null) {
68
+ return (
69
+ <div className={depth > 0 ? 'pd2l' : ''}>
70
+ {Object.entries(obj).map(([k, v]) => (
71
+ <div key={k} className='pd1b'>
72
+ <b>{k}:</b> {renderParsed(v, depth + 1)}
73
+ </div>
74
+ ))}
75
+ </div>
76
+ )
77
+ } else {
78
+ return <span>{String(obj)}</span>
79
+ }
80
+ }
81
+
82
+ const renderValue = (v) => {
83
+ try {
84
+ const parsed = JSON.parse(v)
85
+ return renderParsed(parsed)
86
+ } catch {
87
+ return <span>{v}</span>
88
+ }
89
+ }
90
+
91
+ const renderOSInfo = () => {
92
+ return window.pre.osInfo().map(({ k, v }, i) => (
93
+ <div className='pd1b' key={i + '_os_' + k}>
94
+ <b className='bold'>{k}:</b>
95
+ <span className='mg1l'>
96
+ {renderValue(v)}
97
+ </span>
98
+ </div>
99
+ ))
60
100
  }
61
101
 
62
102
  const { infoModalTab, commandLineHelp } = props
@@ -100,7 +140,7 @@ export default auto(function InfoModal (props) {
100
140
  ...env
101
141
  }
102
142
  const title = (
103
- <div className='ant-modal-confirm-title font16'>
143
+ <div className='custom-modal-close-confirm-title font16'>
104
144
  <InfoCircleOutlined className='font20 mg1r' /> {e('about')} {name}
105
145
  </div>
106
146
  )
@@ -108,9 +148,7 @@ export default auto(function InfoModal (props) {
108
148
  title,
109
149
  width: window.innerWidth - 100,
110
150
  maskClosable: true,
111
- okText: e('ok'),
112
151
  onCancel: onCloseAbout,
113
- footer: null,
114
152
  open: true,
115
153
  wrapClassName: 'info-modal'
116
154
  }
@@ -123,18 +161,18 @@ export default auto(function InfoModal (props) {
123
161
  <LogoElem />
124
162
  <p className='mg2b'>{e('desc')}</p>
125
163
  <RunningTime />
126
- <p className='mg1b'>
127
- <UserOutlined /> <b className='mg1r'>{e('author')} ➾</b>
128
- <Link to={authorUrl} className='mg1l'>
129
- {authorName} ({email})
130
- </Link>
131
- </p>
132
164
  <p className='mg1b'>
133
165
  <HomeOutlined /> <b>{e('homepage')}/{e('download')} ➾</b>
134
166
  <Link to={homepage} className='mg1l'>
135
167
  {homepage}
136
168
  </Link>
137
169
  </p>
170
+ <p className='mg1b'>
171
+ <UserOutlined /> <b className='mg1r'>{e('author')} ➾</b>
172
+ <Link to={authorUrl} className='mg1l'>
173
+ {authorName} ({email})
174
+ </Link>
175
+ </p>
138
176
  <p className='mg1b'>
139
177
  <GithubOutlined /> <b className='mg1r'>github ➾</b>
140
178
  <Link to={link} className='mg1l'>
@@ -180,12 +218,6 @@ export default auto(function InfoModal (props) {
180
218
  <p className='mg1b'>
181
219
  <InfoCircleOutlined /> <b className='mg1r'>{window.store.installSrc}</b>
182
220
  </p>
183
- <p className='mg1b'>
184
- <JavaScriptOutlined /> <b className='mg1r'>Powered by</b>
185
- <Link to='https://github.com/tylerlong/manate'>
186
- manate
187
- </Link>
188
- </p>
189
221
  {renderCheckUpdate()}
190
222
  </>
191
223
  )
@@ -223,20 +255,7 @@ export default auto(function InfoModal (props) {
223
255
  {
224
256
  key: infoTabs.os,
225
257
  label: e('os'),
226
- children: window.pre.osInfo().map(({ k, v }, i) => {
227
- return (
228
- <div className='pd1b' key={i + '_os_' + k}>
229
- <b className='bold'>{k}</b>:
230
- <span className='mg1l'>
231
- {
232
- v.length > 30
233
- ? <pre>{formatJSON(v)}</pre>
234
- : v
235
- }
236
- </span>
237
- </div>
238
- )
239
- })
258
+ children: <div>{renderOSInfo()}</div>
240
259
  }
241
260
  ]
242
261
 
@@ -5,10 +5,3 @@
5
5
  border-radius 60% 40% 30% 70% / 60% 30% 70% 40%
6
6
  transition all 1s ease-in-out
7
7
  z-index 5
8
-
9
- .info-modal
10
- .ant-modal-header
11
- border none
12
- .ant-modal-body
13
- padding 52px
14
- padding-top 0
@@ -12,26 +12,16 @@ import {
12
12
  LeftOutlined,
13
13
  RightOutlined
14
14
  } from '@ant-design/icons'
15
- import {
16
- SingleIcon,
17
- TwoColumnsIcon,
18
- ThreeColumnsIcon,
19
- TwoRowsIcon,
20
- ThreeRowsIcon,
21
- Grid2x2Icon,
22
- TwoRowsRightIcon,
23
- TwoColumnsBottomIcon
24
- } from '../icons/split-icons'
25
15
  import { Dropdown } from 'antd'
26
16
  import Tab from './tab'
17
+ import LayoutMenu from './layout-menu'
27
18
  import './tabs.styl'
28
19
  import {
29
20
  tabWidth,
30
21
  tabMargin,
31
22
  extraTabWidth,
32
23
  windowControlWidth,
33
- isMacJs,
34
- splitMapDesc
24
+ isMacJs
35
25
  } from '../../common/constants'
36
26
  import WindowControl from './window-control'
37
27
  import AddBtn from './add-btn'
@@ -39,8 +29,6 @@ import AppDrag from './app-drag'
39
29
  import NoSession from './no-session'
40
30
  import classNames from 'classnames'
41
31
 
42
- const e = window.translate
43
-
44
32
  export default class Tabs extends Component {
45
33
  constructor (props) {
46
34
  super(props)
@@ -183,10 +171,6 @@ export default class Tabs extends Component {
183
171
  window.store['activeTabId' + this.props.batch] = id
184
172
  }
185
173
 
186
- handleChangeLayout = ({ key }) => {
187
- window.store.setLayout(key)
188
- }
189
-
190
174
  renderAddBtn = () => {
191
175
  const cls = classNames(
192
176
  'pointer tabs-add-btn font16',
@@ -324,48 +308,12 @@ export default class Tabs extends Component {
324
308
  )
325
309
  }
326
310
 
327
- getLayoutIcon = (layout) => {
328
- const iconMaps = {
329
- single: SingleIcon,
330
- twoColumns: TwoColumnsIcon,
331
- threeColumns: ThreeColumnsIcon,
332
- twoRows: TwoRowsIcon,
333
- threeRows: ThreeRowsIcon,
334
- grid2x2: Grid2x2Icon,
335
- twoRowsRight: TwoRowsRightIcon,
336
- twoColumnsBottom: TwoColumnsBottomIcon
337
- }
338
- return iconMaps[layout]
339
- }
340
-
341
311
  renderLayoutMenu = () => {
342
- if (!this.shouldRenderWindowControl()) {
343
- return null
344
- }
345
- const items = Object.keys(splitMapDesc).map((t) => {
346
- const v = splitMapDesc[t]
347
- const Icon = this.getLayoutIcon(v)
348
- return {
349
- key: t,
350
- label: (
351
- <span>
352
- <Icon /> {e(v)}
353
- </span>
354
- ),
355
- onClick: () => this.handleChangeLayout({ key: t })
356
- }
357
- })
358
- const v = splitMapDesc[this.props.layout]
359
- const Icon = this.getLayoutIcon(v)
360
312
  return (
361
- <Dropdown
362
- menu={{ items }}
363
- placement='bottomRight'
364
- >
365
- <span className='tabs-dd-icon layout-dd-icon mg1l'>
366
- <Icon /> <DownOutlined />
367
- </span>
368
- </Dropdown>
313
+ <LayoutMenu
314
+ layout={this.props.layout}
315
+ visible={this.shouldRenderWindowControl()}
316
+ />
369
317
  )
370
318
  }
371
319
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Layout and Workspace menu dropdown component
3
+ */
4
+
5
+ import React, { useState } from 'react'
6
+ import { Dropdown, Tabs } from 'antd'
7
+ import {
8
+ DownOutlined,
9
+ AppstoreOutlined,
10
+ LayoutOutlined
11
+ } from '@ant-design/icons'
12
+ import { splitMapDesc } from '../../common/constants'
13
+ import LayoutSelect, { getLayoutIcon } from './layout-select'
14
+ import WorkspaceSelect from './workspace-select'
15
+ import HelpIcon from '../common/help-icon'
16
+
17
+ const e = window.translate
18
+
19
+ export default function LayoutMenu (props) {
20
+ const { layout, visible } = props
21
+ const [activeTab, setActiveTab] = useState('layout')
22
+
23
+ if (!visible) {
24
+ return null
25
+ }
26
+
27
+ const tabItems = [
28
+ {
29
+ key: 'layout',
30
+ label: (
31
+ <span>
32
+ <LayoutOutlined /> {e('layout')}
33
+ </span>
34
+ )
35
+ },
36
+ {
37
+ key: 'workspaces',
38
+ label: (
39
+ <span>
40
+ <AppstoreOutlined /> {e('workspaces')}
41
+ <HelpIcon link='https://github.com/electerm/electerm/wiki/Workspace-Feature' />
42
+ </span>
43
+ )
44
+ }
45
+ ]
46
+
47
+ const v = splitMapDesc[layout]
48
+ const Icon = getLayoutIcon(v)
49
+
50
+ const dropdownContent = (
51
+ <div className='layout-workspace-dropdown'>
52
+ <Tabs
53
+ items={tabItems}
54
+ size='small'
55
+ activeKey={activeTab}
56
+ onChange={setActiveTab}
57
+ />
58
+ {activeTab === 'layout'
59
+ ? <LayoutSelect layout={layout} />
60
+ : <WorkspaceSelect store={window.store} />}
61
+ </div>
62
+ )
63
+
64
+ return (
65
+ <Dropdown
66
+ popupRender={() => dropdownContent}
67
+ placement='bottomRight'
68
+ trigger={['click']}
69
+ >
70
+ <span className='tabs-dd-icon layout-dd-icon mg1l'>
71
+ <Icon /> <DownOutlined />
72
+ </span>
73
+ </Dropdown>
74
+ )
75
+ }