@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
@@ -244,3 +244,67 @@
244
244
  max-height 300px
245
245
  overflow-y auto
246
246
 
247
+ // Layout and Workspace dropdown styles
248
+ .layout-workspace-dropdown
249
+ background var(--main)
250
+ border-radius 4px
251
+ box-shadow 0 2px 8px rgba(0, 0, 0, 0.15)
252
+ min-width 200px
253
+ padding 8px
254
+ .ant-tabs-nav
255
+ margin-bottom 8px
256
+
257
+ .layout-menu-content
258
+ max-height 300px
259
+ overflow-y auto
260
+
261
+ .layout-menu-item
262
+ padding 6px 12px
263
+ cursor pointer
264
+ border-radius 4px
265
+ display flex
266
+ align-items center
267
+ gap 8px
268
+ color var(--text)
269
+ &:hover
270
+ background var(--main-dark)
271
+ &.active
272
+ background var(--primary)
273
+ color #fff
274
+
275
+ .workspace-menu-content
276
+ max-height 300px
277
+ overflow-y auto
278
+
279
+ .workspace-save-btn
280
+ margin-bottom 8px
281
+
282
+ .workspace-list
283
+ display flex
284
+ flex-direction column
285
+ gap 4px
286
+
287
+ .workspace-item
288
+ padding 6px 12px
289
+ cursor pointer
290
+ border-radius 4px
291
+ display flex
292
+ align-items center
293
+ justify-content space-between
294
+ color var(--text)
295
+ &:hover
296
+ background var(--main-dark)
297
+ .workspace-delete-icon
298
+ opacity 0
299
+ color var(--text-dark)
300
+ &:hover
301
+ color var(--error)
302
+ &:hover .workspace-delete-icon
303
+ opacity 1
304
+
305
+ .workspace-name
306
+ flex 1
307
+ overflow hidden
308
+ text-overflow ellipsis
309
+ white-space nowrap
310
+
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Workspace save modal component - standalone modal
3
+ */
4
+
5
+ import React, { useState } from 'react'
6
+ import { auto } from 'manate/react'
7
+ import Modal from '../common/modal'
8
+ import { Input, Select, Button, Space, message, Radio } from 'antd'
9
+ import { SaveOutlined, EditOutlined } from '@ant-design/icons'
10
+
11
+ const e = window.translate
12
+
13
+ export default auto(function WorkspaceSaveModal ({ store }) {
14
+ const { workspaceSaveModalVisible, workspaces } = store
15
+ const [name, setName] = useState('')
16
+ const [selectedId, setSelectedId] = useState(null)
17
+ const [saveMode, setSaveMode] = useState('new') // 'new' or 'overwrite'
18
+
19
+ if (!workspaceSaveModalVisible) {
20
+ return null
21
+ }
22
+
23
+ function handleClose () {
24
+ window.store.workspaceSaveModalVisible = false
25
+ }
26
+
27
+ function handleSave () {
28
+ if (saveMode === 'new') {
29
+ if (!name.trim()) {
30
+ message.error(e('name needed'))
31
+ return
32
+ }
33
+ window.store.saveWorkspace(name.trim())
34
+ message.success(e('saved'))
35
+ } else {
36
+ if (!selectedId) {
37
+ message.error('please Select Workspace')
38
+ return
39
+ }
40
+ const ws = workspaces.find(w => w.id === selectedId)
41
+ window.store.saveWorkspace(ws?.name || name, selectedId)
42
+ message.success(e('saved'))
43
+ }
44
+ setName('')
45
+ setSelectedId(null)
46
+ setSaveMode('new')
47
+ handleClose()
48
+ }
49
+
50
+ function handleCancel () {
51
+ setName('')
52
+ setSelectedId(null)
53
+ setSaveMode('new')
54
+ handleClose()
55
+ }
56
+
57
+ const options = workspaces.map(w => ({
58
+ label: w.name,
59
+ value: w.id
60
+ }))
61
+
62
+ return (
63
+ <Modal
64
+ title={e('save')}
65
+ open={workspaceSaveModalVisible}
66
+ onCancel={handleCancel}
67
+ footer={null}
68
+ width={400}
69
+ >
70
+ <div className='pd1y'>
71
+ <Space direction='vertical' block>
72
+ <Radio.Group
73
+ value={saveMode}
74
+ onChange={ev => setSaveMode(ev.target.value)}
75
+ >
76
+ <Radio value='new'>
77
+ <SaveOutlined className='mg1r' />
78
+ {e('saveAsNew')}
79
+ </Radio>
80
+ <Radio value='overwrite' disabled={!workspaces.length}>
81
+ <EditOutlined className='mg1r' />
82
+ {e('overwrite')}
83
+ </Radio>
84
+ </Radio.Group>
85
+
86
+ {saveMode === 'new'
87
+ ? (
88
+ <Input
89
+ placeholder={e('name')}
90
+ value={name}
91
+ onChange={e => setName(e.target.value)}
92
+ onPressEnter={handleSave}
93
+ />
94
+ )
95
+ : (
96
+ <Select
97
+ placeholder={e('workspaces')}
98
+ value={selectedId}
99
+ onChange={setSelectedId}
100
+ options={options}
101
+ style={{ width: '100%' }}
102
+ />
103
+ )}
104
+
105
+ <div className='pd1t'>
106
+ <Button type='primary' onClick={handleSave}>
107
+ {e('save')}
108
+ </Button>
109
+ <Button className='mg1l' onClick={handleCancel}>
110
+ {e('cancel')}
111
+ </Button>
112
+ </div>
113
+ </Space>
114
+ </div>
115
+ </Modal>
116
+ )
117
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Workspace select content component
3
+ */
4
+
5
+ import React from 'react'
6
+ import { Button, Empty, Popconfirm } from 'antd'
7
+ import {
8
+ SaveOutlined,
9
+ DeleteOutlined
10
+ } from '@ant-design/icons'
11
+ import { auto } from 'manate/react'
12
+
13
+ const e = window.translate
14
+
15
+ export default auto(function WorkspaceSelect (props) {
16
+ const { store } = props
17
+ const { workspaces } = store
18
+
19
+ function handleLoadWorkspace (id) {
20
+ window.store.loadWorkspace(id)
21
+ }
22
+
23
+ function handleDeleteWorkspace (id, ev) {
24
+ ev.stopPropagation()
25
+ window.store.deleteWorkspace(id)
26
+ }
27
+
28
+ function handleSaveClick () {
29
+ window.store.workspaceSaveModalVisible = true
30
+ }
31
+
32
+ return (
33
+ <div className='workspace-menu-content'>
34
+ <div className='workspace-save-btn pd1b'>
35
+ <Button
36
+ type='primary'
37
+ icon={<SaveOutlined />}
38
+ size='small'
39
+ onClick={handleSaveClick}
40
+ block
41
+ >
42
+ {e('save')}
43
+ </Button>
44
+ </div>
45
+ {workspaces.length === 0
46
+ ? (
47
+ <Empty
48
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
49
+ description='No items'
50
+ />
51
+ )
52
+ : (
53
+ <div className='workspace-list'>
54
+ {workspaces.map(ws => (
55
+ <div
56
+ key={ws.id}
57
+ className='workspace-item'
58
+ onClick={() => handleLoadWorkspace(ws.id)}
59
+ >
60
+ <span className='workspace-name'>{ws.name}</span>
61
+ <Popconfirm
62
+ title={e('del') + '?'}
63
+ onConfirm={(ev) => handleDeleteWorkspace(ws.id, ev)}
64
+ onCancel={(ev) => ev.stopPropagation()}
65
+ okText={e('ok')}
66
+ cancelText={e('cancel')}
67
+ >
68
+ <DeleteOutlined
69
+ className='workspace-delete-icon'
70
+ onClick={(ev) => ev.stopPropagation()}
71
+ />
72
+ </Popconfirm>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ )}
77
+ </div>
78
+ )
79
+ })
@@ -34,7 +34,13 @@ export default class AttachAddonCustom extends AttachAddon {
34
34
  }
35
35
 
36
36
  onMsg = (ev) => {
37
- this.trzsz.processServerOutput(ev.data)
37
+ // When in alternate screen mode (like vim, less, or TUI apps like Claude Code),
38
+ // bypass trzsz processing to avoid interference with the application's display
39
+ if (this.term?.buffer?.active?.type === 'alternate') {
40
+ this.writeToTerminal(ev.data)
41
+ } else {
42
+ this.trzsz.processServerOutput(ev.data)
43
+ }
38
44
  }
39
45
 
40
46
  writeToTerminal = (data) => {
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import { useEffect, useState } from 'react'
6
- import { Modal, Form, Button } from 'antd'
6
+ import { Form, Button } from 'antd'
7
+ import Modal from '../common/modal'
7
8
  import InputAutoFocus from '../common/input-auto-focus'
8
9
  import wait from '../../common/wait'
9
10
 
@@ -178,7 +178,6 @@ class Term extends Component {
178
178
  clearTimeout(this.timers[k])
179
179
  this.timers[k] = null
180
180
  })
181
- this.timers = null
182
181
  this.onClose = true
183
182
  if (this.socket) {
184
183
  this.socket.close()
@@ -1321,7 +1320,7 @@ class Term extends Component {
1321
1320
  }
1322
1321
  this.socketCloseWarning = notification.warning({
1323
1322
  key,
1324
- message: e('socketCloseTip'),
1323
+ title: e('socketCloseTip'),
1325
1324
  duration: 30,
1326
1325
  description: (
1327
1326
  <div className='pd2y'>
@@ -4,7 +4,8 @@
4
4
 
5
5
  import { PureComponent } from 'react'
6
6
  import TextEditorForm from './text-editor-form'
7
- import { Spin, Modal } from 'antd'
7
+ import { Spin } from 'antd'
8
+ import Modal from '../common/modal'
8
9
  import resolve from '../../common/resolve'
9
10
  import { refsStatic, refs } from '../common/ref'
10
11
 
@@ -5,7 +5,8 @@ import {
5
5
  SearchOutlined
6
6
  } from '@ant-design/icons'
7
7
  import buildGroupData from '../bookmark-form/common/bookmark-group-tree-format'
8
- import { Tree, Modal, Button, Input } from 'antd'
8
+ import { Tree, Button, Input } from 'antd'
9
+ import Modal from '../common/modal'
9
10
  import { auto } from 'manate/react'
10
11
  const e = window.translate
11
12
 
@@ -10,9 +10,9 @@ import {
10
10
  import {
11
11
  Spin,
12
12
  message,
13
- Modal,
14
13
  Tag
15
14
  } from 'antd'
15
+ import Modal from '../common/modal'
16
16
  import * as ls from '../../common/safe-local-storage'
17
17
  import { copy } from '../../common/clipboard'
18
18
  import resolutions from '../rdp/resolutions'
@@ -250,7 +250,7 @@ export default class VncSession extends RdpSession {
250
250
  title: e('credentialsRequired'),
251
251
  content: this.renderForm(['password']),
252
252
  footer: null,
253
- visible: true
253
+ open: true
254
254
  }
255
255
  return (
256
256
  <Modal
@@ -3,11 +3,9 @@
3
3
  */
4
4
  import React, { useState } from 'react'
5
5
  import WidgetForm from './widget-form'
6
- import {
7
- message
8
- } from 'antd'
6
+ import { showMsg } from './widget-notification-with-details'
9
7
 
10
- export default function WidgetControl ({ formData }) {
8
+ export default function WidgetControl ({ formData, widgetInstancesLength }) {
11
9
  const [loading, setLoading] = useState(false)
12
10
  const widget = formData
13
11
  if (!widget.id) {
@@ -18,6 +16,12 @@ export default function WidgetControl ({ formData }) {
18
16
  )
19
17
  }
20
18
 
19
+ // Check if this widget already has a running instance
20
+ // widgetInstancesLength is used to trigger re-render when instances change
21
+ const hasRunningInstance = widgetInstancesLength > 0 && window.store.widgetInstances.some(
22
+ instance => instance.widgetId === widget.id
23
+ )
24
+
21
25
  const handleFormSubmit = async (config) => {
22
26
  setLoading(true)
23
27
  try {
@@ -30,9 +34,9 @@ export default function WidgetControl ({ formData }) {
30
34
  } = result
31
35
  if (!instanceId) {
32
36
  if (success === false) {
33
- message.error('Failed to run widget', error || '')
37
+ showMsg('Failed to run widget', 'error', null, 10, error || '')
34
38
  } else {
35
- message.success(msg || 'Widget run successfully')
39
+ showMsg(msg, 'success', null, 10)
36
40
  }
37
41
  return
38
42
  }
@@ -45,6 +49,7 @@ export default function WidgetControl ({ formData }) {
45
49
  config
46
50
  }
47
51
  window.store.widgetInstances.push(instance)
52
+ showMsg(msg, 'success', result.serverInfo, 10)
48
53
  } catch (err) {
49
54
  console.error('Failed to run widget:', err)
50
55
  } finally {
@@ -58,6 +63,7 @@ export default function WidgetControl ({ formData }) {
58
63
  widget={widget}
59
64
  onSubmit={handleFormSubmit}
60
65
  loading={loading}
66
+ hasRunningInstance={hasRunningInstance}
61
67
  />
62
68
  </div>
63
69
  )
@@ -2,10 +2,10 @@
2
2
  * Widget form component
3
3
  */
4
4
  import React from 'react'
5
- import { Form, Input, InputNumber, Switch, Select, Button, message } from 'antd'
5
+ import { Form, Input, InputNumber, Switch, Select, Button, Tooltip } from 'antd'
6
6
  import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
7
7
 
8
- export default function WidgetForm ({ widget, onSubmit, loading }) {
8
+ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInstance }) {
9
9
  const [form] = Form.useForm()
10
10
 
11
11
  if (!widget) {
@@ -13,17 +13,13 @@ export default function WidgetForm ({ widget, onSubmit, loading }) {
13
13
  }
14
14
 
15
15
  const { info } = widget
16
- const { configs, type } = info
16
+ const { configs, type, singleInstance } = info
17
17
  const isInstanceWidget = type === 'instance'
18
18
  const txt = isInstanceWidget ? 'Start widget' : 'Run widget'
19
+ const isDisabled = loading || (singleInstance && hasRunningInstance)
19
20
 
20
21
  const handleSubmit = async (values) => {
21
- try {
22
- await onSubmit(values)
23
- message.success('Widget started successfully')
24
- } catch (error) {
25
- message.error('Failed to start widget: ' + error.message)
26
- }
22
+ onSubmit(values)
27
23
  }
28
24
 
29
25
  const renderFormItem = (config) => {
@@ -86,7 +82,7 @@ export default function WidgetForm ({ widget, onSubmit, loading }) {
86
82
 
87
83
  return (
88
84
  <div className='widget-form'>
89
- <div className='pd1b'>
85
+ <div className='pd1b alignright'>
90
86
  <h4>{info.name}</h4>
91
87
  <p>{info.description}</p>
92
88
  </div>
@@ -100,14 +96,16 @@ export default function WidgetForm ({ widget, onSubmit, loading }) {
100
96
  <Form.Item
101
97
  {...tailFormItemLayout}
102
98
  >
103
- <Button
104
- type='primary'
105
- htmlType='submit'
106
- loading={loading}
107
- disabled={loading}
108
- >
109
- {txt}
110
- </Button>
99
+ <Tooltip title={isDisabled && singleInstance && hasRunningInstance ? 'Already running, only one instance allowed' : ''}>
100
+ <Button
101
+ type='primary'
102
+ htmlType='submit'
103
+ loading={loading}
104
+ disabled={isDisabled}
105
+ >
106
+ {txt}
107
+ </Button>
108
+ </Tooltip>
111
109
  </Form.Item>
112
110
  </Form>
113
111
  </div>
@@ -1,10 +1,11 @@
1
- import { Popconfirm } from 'antd'
2
- import { CloseOutlined } from '@ant-design/icons'
1
+ import { Popconfirm, Popover } from 'antd'
2
+ import { CloseOutlined, CopyOutlined } from '@ant-design/icons'
3
+ import { copy } from '../../common/clipboard'
3
4
 
4
5
  const e = window.translate
5
6
 
6
7
  export default function WidgetInstance ({ item }) {
7
- const { id, title } = item
8
+ const { id, title, serverInfo } = item
8
9
  const cls = 'item-list-unit'
9
10
  const delProps = {
10
11
  title: e('del'),
@@ -25,17 +26,51 @@ export default function WidgetInstance ({ item }) {
25
26
  cancelText: e('cancel'),
26
27
  placement: 'top'
27
28
  }
29
+ const handleCopy = () => {
30
+ if (serverInfo && serverInfo.url) {
31
+ copy(serverInfo.url)
32
+ }
33
+ }
34
+ const popoverContent = serverInfo
35
+ ? (
36
+ <div>
37
+ <div style={{ display: 'flex', alignItems: 'center' }}>
38
+ <span>URL: {serverInfo.url}</span>
39
+ <CopyOutlined
40
+ className='pointer mg1l'
41
+ onClick={handleCopy}
42
+ />
43
+ </div>
44
+ <div>Path: {serverInfo.path}</div>
45
+ </div>
46
+ )
47
+ : null
48
+ const titleDiv = (
49
+ <div
50
+ title={title}
51
+ className='elli pd1y pd2x list-item-title'
52
+ >
53
+ {title}
54
+ </div>
55
+ )
28
56
  return (
29
57
  <div
30
58
  key={id}
31
59
  className={cls}
32
60
  >
33
- <div
34
- title={title}
35
- className='elli pd1y pd2x list-item-title'
36
- >
37
- {title}
38
- </div>
61
+ {
62
+ serverInfo
63
+ ? (
64
+ <Popover
65
+ content={popoverContent}
66
+ trigger='hover'
67
+ placement='top'
68
+ >
69
+ {titleDiv}
70
+ </Popover>
71
+ )
72
+ : titleDiv
73
+ }
39
74
  <Popconfirm
40
75
  {...popProps}
41
76
  >
@@ -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: {serverInfo.url}</span>
19
+ <CopyOutlined
20
+ className='pointer mg1l'
21
+ onClick={handleCopy}
22
+ />
23
+ </div>
24
+ <div>Path: {serverInfo.path}</div>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ notification[type]({
30
+ message,
31
+ description: desc,
32
+ duration
33
+ })
34
+ }
@@ -32,7 +32,9 @@ body
32
32
  word-break break-all
33
33
 
34
34
  a
35
- color var(--primary)
35
+ color var(--text)
36
+ &:hover
37
+ color var(--text-dark)
36
38
 
37
39
  .cap
38
40
  text-transform capitalize
@@ -95,10 +95,10 @@ for $i, $index in 5 16 32
95
95
  overflow hidden
96
96
 
97
97
  .overscroll
98
- overflow scroll
98
+ overflow auto
99
99
 
100
100
  .overscroll-y
101
- overflow-y scroll
101
+ overflow-y auto
102
102
 
103
103
  .relative
104
104
  position relative
@@ -4,7 +4,7 @@
4
4
 
5
5
  import handleError from '../common/error-handler'
6
6
  import { Modal } from 'antd'
7
- import { debounce, some, get } from 'lodash-es'
7
+ import { debounce, some, get, pickBy } from 'lodash-es'
8
8
  import {
9
9
  modals,
10
10
  leftSidebarWidthKey,
@@ -219,27 +219,31 @@ export default Store => {
219
219
  delete p.name
220
220
  delete p.id
221
221
  if (type === connectionMap.rdp) {
222
+ const filtered = pickBy(p.rdp, (value) => value !== undefined && value !== '')
222
223
  return {
223
224
  ...tab,
224
- ...p.rdp
225
+ ...filtered
225
226
  }
226
227
  } else if (type === connectionMap.vnc) {
228
+ const filtered = pickBy(p.vnc, (value) => value !== undefined && value !== '')
227
229
  return {
228
230
  ...tab,
229
- ...p.vnc
231
+ ...filtered
230
232
  }
231
233
  } else if (type === connectionMap.telnet) {
234
+ const filtered = pickBy(p.telnet, (value) => value !== undefined && value !== '')
232
235
  return {
233
236
  ...tab,
234
- ...p.telnet
237
+ ...filtered
235
238
  }
236
239
  }
237
240
  delete p.rdp
238
241
  delete p.vnc
239
242
  delete p.telnet
243
+ const filtered = pickBy(p, (value) => value !== undefined && value !== '')
240
244
  return {
241
245
  ...tab,
242
- ...p
246
+ ...filtered
243
247
  }
244
248
  }
245
249
  Store.prototype.applyProfileToTabs = function (tab) {
@@ -75,6 +75,10 @@ export default () => {
75
75
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
76
76
  terminalCommandHistory: new Set(ls.getItemJSON(cmdHistoryKey, [])),
77
77
 
78
+ // workspaces
79
+ workspaces: [],
80
+ workspaceSaveModalVisible: false,
81
+
78
82
  // init session control
79
83
  selectedSessions: [],
80
84
  sessionModalVisible: false,