@electerm/electerm-react 2.10.6 → 2.10.27

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.
@@ -13,10 +13,14 @@ import {
13
13
  aiConfigWikiLink
14
14
  } from '../../common/constants'
15
15
  import Password from '../common/password'
16
+ import AiHistory, { addHistoryItem } from './ai-history'
16
17
 
17
18
  // Comprehensive API provider configurations
18
19
  import providers from './providers'
19
20
 
21
+ const STORAGE_KEY_CONFIG = 'ai_config_history'
22
+ const EVENT_NAME_CONFIG = 'ai-config-history-update'
23
+
20
24
  const e = window.translate
21
25
  const defaultRoles = [
22
26
  {
@@ -66,14 +70,27 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
66
70
 
67
71
  const handleSubmit = async (values) => {
68
72
  onSubmit(values)
73
+ addHistoryItem(STORAGE_KEY_CONFIG, values, EVENT_NAME_CONFIG)
74
+ }
75
+
76
+ function handleSelectHistory (item) {
77
+ if (item && typeof item === 'object') {
78
+ form.setFieldsValue(item)
79
+ }
80
+ }
81
+
82
+ function renderHistoryItem (item) {
83
+ if (!item || typeof item !== 'object') return { label: 'Unknown', title: 'Unknown' }
84
+ const model = item.modelAI || 'Default Model'
85
+ const rolePrefix = item.roleAI ? item.roleAI.substring(0, 15) + '...' : ''
86
+ const label = `[${model}] ${rolePrefix}`
87
+ const title = `Model: ${item.modelAI}\nRole: ${item.roleAI}\nURL: ${item.baseURLAI}`
88
+ return { label, title }
69
89
  }
70
90
 
71
91
  function handleChange (v) {
72
92
  const options = getModelOptions(v)
73
93
  setModelOptions(options)
74
- form.setFieldsValue({
75
- modelAI: options[0]?.value || ''
76
- })
77
94
  }
78
95
 
79
96
  if (!showAIConfig) {
@@ -199,6 +216,12 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
199
216
  </Button>
200
217
  </Form.Item>
201
218
  </Form>
219
+ <AiHistory
220
+ storageKey={STORAGE_KEY_CONFIG}
221
+ eventName={EVENT_NAME_CONFIG}
222
+ onSelect={handleSelectHistory}
223
+ renderItem={renderHistoryItem}
224
+ />
202
225
  <AiCache />
203
226
  </>
204
227
  )
@@ -0,0 +1,46 @@
1
+ import { Tag, Tooltip } from 'antd'
2
+
3
+ export default function AiHistoryItem (props) {
4
+ const {
5
+ item,
6
+ onSelect,
7
+ onDelete,
8
+ renderItem
9
+ } = props
10
+
11
+ // If a custom render function is provided, use it to get the display string and title
12
+ // otherwise assume it's a string
13
+ let displayItem = ''
14
+ let fullItem = ''
15
+
16
+ if (renderItem) {
17
+ const renderedInfo = renderItem(item)
18
+ displayItem = renderedInfo.label
19
+ fullItem = renderedInfo.title || renderedInfo.label
20
+ } else {
21
+ fullItem = item
22
+ displayItem = item.length > 50 ? `${item.slice(0, 50)}...` : item
23
+ }
24
+
25
+ const isLong = fullItem.length > 50
26
+ const tagElem = (
27
+ <Tag
28
+ closable
29
+ onClose={(event) => onDelete(item, event)}
30
+ onClick={() => onSelect(item)}
31
+ className='pointer'
32
+ >
33
+ {displayItem}
34
+ </Tag>
35
+ )
36
+
37
+ return isLong
38
+ ? (
39
+ <Tooltip title={fullItem}>
40
+ {tagElem}
41
+ </Tooltip>
42
+ )
43
+ : (
44
+ tagElem
45
+ )
46
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Generic AI history component
3
+ */
4
+ import { useState, useEffect } from 'react'
5
+ import { Space } from 'antd'
6
+ import { HistoryOutlined } from '@ant-design/icons'
7
+ import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
8
+ import AiHistoryItem from './ai-history-item'
9
+
10
+ const MAX_HISTORY = 20
11
+ const e = window.translate
12
+
13
+ export function getHistory (storageKey) {
14
+ return getItemJSON(storageKey, [])
15
+ }
16
+
17
+ export function addHistoryItem (storageKey, itemData, eventName) {
18
+ if (!itemData) return
19
+
20
+ // Standardize the check: if itemData is an object, we serialize it
21
+ // But wait, the comparison should be stable. We can rely on a unique identifier
22
+ // or a stringified version to avoid duplicates.
23
+ let history = getHistory(storageKey)
24
+ const itemStr = typeof itemData === 'string' ? itemData.trim() : JSON.stringify(itemData)
25
+
26
+ if (!itemStr) return
27
+
28
+ history = history.filter(h => {
29
+ const hStr = typeof h === 'string' ? h.trim() : JSON.stringify(h)
30
+ return hStr !== itemStr
31
+ })
32
+
33
+ // use original data structure to save
34
+ const dataToSave = typeof itemData === 'string' ? itemData.trim() : itemData
35
+
36
+ history.unshift(dataToSave)
37
+ if (history.length > MAX_HISTORY) {
38
+ history = history.slice(0, MAX_HISTORY)
39
+ }
40
+ setItemJSON(storageKey, history)
41
+
42
+ // Custom event to trigger update
43
+ if (eventName) {
44
+ window.dispatchEvent(new Event(eventName))
45
+ }
46
+ }
47
+
48
+ export default function AiHistory (props) {
49
+ const { onSelect, storageKey, eventName, renderItem } = props
50
+ const [history, setHistory] = useState([])
51
+
52
+ const loadHistory = () => {
53
+ setHistory(getHistory(storageKey))
54
+ }
55
+
56
+ useEffect(() => {
57
+ loadHistory()
58
+ if (eventName) {
59
+ window.addEventListener(eventName, loadHistory)
60
+ return () => {
61
+ window.removeEventListener(eventName, loadHistory)
62
+ }
63
+ }
64
+ }, [storageKey, eventName])
65
+
66
+ const handleDelete = (item, event) => {
67
+ event.preventDefault()
68
+ event.stopPropagation()
69
+ const itemStr = typeof item === 'string' ? item : JSON.stringify(item)
70
+ const newHistory = history.filter(h => {
71
+ const hStr = typeof h === 'string' ? h : JSON.stringify(h)
72
+ return hStr !== itemStr
73
+ })
74
+ setHistory(newHistory)
75
+ setItemJSON(storageKey, newHistory)
76
+ }
77
+
78
+ if (!history.length) {
79
+ return null
80
+ }
81
+
82
+ return (
83
+ <div className='ai-bookmark-history pd1b'>
84
+ <div className='pd1b text-muted'>
85
+ <HistoryOutlined className='mg1r' />
86
+ <span className='mg1r'>{e('history') || 'History'}:</span>
87
+ </div>
88
+ <Space size={[8, 8]} wrap>
89
+ {history.map((item, index) => {
90
+ const keyStr = typeof item === 'string' ? item : JSON.stringify(item)
91
+ return (
92
+ <AiHistoryItem
93
+ key={keyStr + index}
94
+ item={item}
95
+ onSelect={onSelect}
96
+ onDelete={handleDelete}
97
+ renderItem={renderItem}
98
+ />
99
+ )
100
+ })}
101
+ </Space>
102
+ </div>
103
+ )
104
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AI-powered bookmark generation form
3
3
  */
4
- import { useState } from 'react'
4
+ import { useState, useEffect } from 'react'
5
5
  import { Button, Input, message, Space, Alert } from 'antd'
6
6
  import {
7
7
  RobotOutlined,
@@ -22,19 +22,28 @@ import Modal from '../common/modal.jsx'
22
22
  import { buildPrompt } from './bookmark-schema.js'
23
23
  import { fixBookmarkData } from './fix-bookmark-default.js'
24
24
  import generate from '../../common/id-with-stamp'
25
+ import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
26
+ import { getItem, setItem } from '../../common/safe-local-storage'
25
27
 
28
+ const STORAGE_KEY_DESC = 'ai_bookmark_description'
29
+ const STORAGE_KEY_HISTORY = 'ai_bookmark_history'
30
+ const EVENT_NAME_HISTORY = 'ai-bookmark-history-update'
26
31
  const { TextArea } = Input
27
32
  const e = window.translate
28
33
 
29
34
  export default function AIBookmarkForm (props) {
30
35
  const { onCancel } = props
31
- const [description, setDescription] = useState('')
36
+ const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
32
37
  const [loading, setLoading] = useState(false)
33
38
  const [showConfirm, setShowConfirm] = useState(false)
34
39
  const [editMode, setEditMode] = useState(false)
35
40
  const [editorText, setEditorText] = useState('')
36
41
  const [selectedCategory, setSelectedCategory] = useState('default')
37
42
 
43
+ useEffect(() => {
44
+ setItem(STORAGE_KEY_DESC, description)
45
+ }, [description])
46
+
38
47
  const handleGenerate = async () => {
39
48
  if (window.store.aiConfigMissing()) {
40
49
  window.store.toggleAIConfig()
@@ -88,6 +97,7 @@ export default function AIBookmarkForm (props) {
88
97
  // set default category when preview opens
89
98
  setSelectedCategory('default')
90
99
  setShowConfirm(true)
100
+ addHistoryItem(STORAGE_KEY_HISTORY, description, EVENT_NAME_HISTORY)
91
101
  }
92
102
  } catch (error) {
93
103
  console.error('AI bookmark generation error:', error)
@@ -148,7 +158,7 @@ export default function AIBookmarkForm (props) {
148
158
  await createBookmark(item)
149
159
  }
150
160
  setShowConfirm(false)
151
- setDescription('')
161
+ setDescription('') // Clear description only on successful creation
152
162
  message.success(e('Done'))
153
163
  }
154
164
 
@@ -157,7 +167,6 @@ export default function AIBookmarkForm (props) {
157
167
  }
158
168
 
159
169
  const handleCancel = () => {
160
- setDescription('')
161
170
  if (onCancel) {
162
171
  onCancel()
163
172
  }
@@ -297,6 +306,11 @@ export default function AIBookmarkForm (props) {
297
306
  <div className='pd1b'>
298
307
  <TextArea {...textAreaProps} />
299
308
  </div>
309
+ <AiHistory
310
+ storageKey={STORAGE_KEY_HISTORY}
311
+ eventName={EVENT_NAME_HISTORY}
312
+ onSelect={setDescription}
313
+ />
300
314
  <div className='pd1t'>
301
315
  <Button {...generateBtnProps}>
302
316
  {e('submit')}
@@ -120,6 +120,7 @@ const bookmarkSchema = {
120
120
  user: 'string - username',
121
121
  secure: 'boolean - use secure FTP (FTPS), default is false',
122
122
  password: 'string - password',
123
+ encode: 'string - charset for file names, default is utf-8',
123
124
  title: 'string - bookmark title',
124
125
  profile: 'string - profile id',
125
126
  description: 'string - bookmark description'
@@ -137,6 +138,18 @@ const bookmarkSchema = {
137
138
  description: 'string - bookmark description',
138
139
  startDirectoryLocal: 'string - local starting directory',
139
140
  runScripts: 'array - run scripts after connected ({delay,script})'
141
+ },
142
+ spice: {
143
+ type: 'spice',
144
+ host: 'string (required) - hostname or IP address',
145
+ port: 'number (default: 5900) - Spice port',
146
+ password: 'string - Spice password',
147
+ title: 'string - bookmark title',
148
+ viewOnly: 'boolean - view only mode, default is false',
149
+ scaleViewport: 'boolean - scale viewport to window, default is true',
150
+ description: 'string - bookmark description',
151
+ profile: 'string - profile id',
152
+ proxy: 'string - proxy address (socks5://...)'
140
153
  }
141
154
  }
142
155
 
@@ -15,6 +15,7 @@ const ftpConfig = {
15
15
  user: '',
16
16
  password: '',
17
17
  secure: false,
18
+ encode: 'utf-8',
18
19
  ...getAuthTypeDefault(props)
19
20
  })
20
21
  },
@@ -32,6 +33,7 @@ const ftpConfig = {
32
33
  { type: 'input', name: 'user', label: () => e('username') },
33
34
  { type: 'password', name: 'password', label: () => e('password') },
34
35
  { type: 'switch', name: 'secure', label: () => e('secure'), valuePropName: 'checked' },
36
+ commonFields.encode,
35
37
  commonFields.type
36
38
  ]
37
39
  }
@@ -2,7 +2,7 @@ import { formItemLayout } from '../../../common/form-layout.js'
2
2
  import { terminalSpiceType } from '../../../common/constants.js'
3
3
  import { createBaseInitValues, getAuthTypeDefault } from '../common/init-values.js'
4
4
  import { isEmpty } from 'lodash-es'
5
- import { commonFields, connectionHoppingTab } from './common-fields.js'
5
+ import { commonFields } from './common-fields.js'
6
6
 
7
7
  const e = window.translate
8
8
 
@@ -36,8 +36,7 @@ const spiceConfig = {
36
36
  commonFields.proxy,
37
37
  commonFields.type
38
38
  ]
39
- },
40
- connectionHoppingTab()
39
+ }
41
40
  ]
42
41
  }
43
42
 
@@ -118,7 +118,6 @@ export default class BookmarkIndex2 extends PureComponent {
118
118
  }
119
119
  return (
120
120
  <Button
121
- type='primary'
122
121
  size='small'
123
122
  className='mg2l create-ai-btn'
124
123
  icon={<RobotOutlined />}
@@ -162,7 +161,7 @@ export default class BookmarkIndex2 extends PureComponent {
162
161
  const keys = Object.keys(sessionConfig)
163
162
  return (
164
163
  <div className='form-wrap pd1x'>
165
- <div className='form-title pd1t pd1x pd2b'>
164
+ <div className='form-title pd1t pd1x pd2b bold'>
166
165
  <BookOutlined className='mg1r' />
167
166
  <span>
168
167
  {((!isNew ? e('edit') : e('new')) + ' ' + e(settingMap.bookmarks))}
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useEffect, useRef } from 'react'
2
- import { CloseOutlined } from '@ant-design/icons'
2
+ import { CloseOutlined, CopyOutlined } from '@ant-design/icons'
3
3
  import classnames from 'classnames'
4
4
  import generateId from '../../common/uid'
5
5
  import { messageIcons } from '../../common/icon-helpers.jsx'
6
+ import { copy } from '../../common/clipboard'
6
7
  import './notification.styl'
7
8
 
8
9
  const notifications = []
@@ -69,6 +70,19 @@ export function NotificationContainer () {
69
70
  )
70
71
  }
71
72
 
73
+ function getTextFromReactChildren (children) {
74
+ if (typeof children === 'string' || typeof children === 'number') {
75
+ return String(children)
76
+ }
77
+ if (React.isValidElement(children)) {
78
+ return getTextFromReactChildren(children.props.children)
79
+ }
80
+ if (Array.isArray(children)) {
81
+ return children.map(getTextFromReactChildren).join('\n')
82
+ }
83
+ return ''
84
+ }
85
+
72
86
  function NotificationItem ({ message, description, type, onClose, duration = 18.5 }) {
73
87
  const timeoutRef = useRef(null)
74
88
 
@@ -97,6 +111,12 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
97
111
  }
98
112
  }
99
113
 
114
+ const handleCopy = (text, e) => {
115
+ e.stopPropagation()
116
+ const textToCopy = getTextFromReactChildren(text)
117
+ copy(textToCopy)
118
+ }
119
+
100
120
  const className = classnames('notification', type)
101
121
 
102
122
  return (
@@ -109,8 +129,20 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
109
129
  <div className='notification-message'>
110
130
  <div className='notification-icon'>{messageIcons[type]}</div>
111
131
  <div className='notification-title' title={message}>{message}</div>
132
+ <CopyOutlined
133
+ className='notification-copy-icon'
134
+ onClick={(e) => handleCopy(message, e)}
135
+ />
112
136
  </div>
113
- {description && <div className='notification-description'>{description}</div>}
137
+ {description && (
138
+ <div className='notification-description'>
139
+ {description}
140
+ <CopyOutlined
141
+ className='notification-copy-icon'
142
+ onClick={(e) => handleCopy(description, e)}
143
+ />
144
+ </div>
145
+ )}
114
146
  </div>
115
147
  <CloseOutlined className='notification-close' onClick={onClose} />
116
148
  </div>
@@ -33,9 +33,10 @@
33
33
  top 10px
34
34
 
35
35
  .notification-description
36
- word-wrap break-all
36
+ word-wrap break-word
37
37
  max-height 200px
38
38
  overflow auto
39
+ padding 10px 0 0 0
39
40
 
40
41
  .notification-close
41
42
  position absolute
@@ -48,4 +49,19 @@
48
49
  cursor pointer
49
50
  padding 0
50
51
  &:hover
51
- color var(--text)
52
+ color var(--text)
53
+
54
+ .notification-copy-icon
55
+ cursor pointer
56
+ font-size 14px
57
+ position absolute
58
+ right 10px
59
+ top 45px
60
+ margin-left 8px
61
+ display none
62
+ .notification-message .notification-copy-icon
63
+ right 38px
64
+ top 14px
65
+ .notification-message:hover .notification-copy-icon,
66
+ .notification-description:hover .notification-copy-icon
67
+ display inline-block
@@ -1,10 +1,3 @@
1
- .common-err-desc
2
- &:hover
3
- text-overflow clip
4
- overflow-x hidden
5
- white-space pre
6
- max-height 300px
7
- overflow scroll
8
1
 
9
2
  .error-wrapper
10
3
  background var(--main)
@@ -6,14 +6,15 @@ import { handleErr } from '../../common/fetch'
6
6
  import {
7
7
  statusMap
8
8
  } from '../../common/constants'
9
- import {
10
- Spin,
11
- Select
12
- } from 'antd'
13
9
  import {
14
10
  ReloadOutlined,
15
11
  EditOutlined
16
12
  } from '@ant-design/icons'
13
+ import {
14
+ Spin,
15
+ Select,
16
+ Switch
17
+ } from 'antd'
17
18
  import * as ls from '../../common/safe-local-storage'
18
19
  import scanCode from './code-scan'
19
20
  import resolutions from './resolutions'
@@ -46,10 +47,13 @@ export default class RdpSession extends PureComponent {
46
47
  constructor (props) {
47
48
  const id = `rdp-reso-${props.tab.host}`
48
49
  const resObj = ls.getItemJSON(id, resolutions[1])
50
+ const scaleViewportId = `rdp-scale-view-${props.tab.host}`
51
+ const scaleViewport = ls.getItemJSON(scaleViewportId, false)
49
52
  super(props)
50
53
  this.canvasRef = createRef()
51
54
  this.state = {
52
55
  loading: false,
56
+ scaleViewport,
53
57
  ...resObj
54
58
  }
55
59
  this.session = null
@@ -76,7 +80,7 @@ export default class RdpSession extends PureComponent {
76
80
  }
77
81
  }
78
82
 
79
- runInitScript = () => {}
83
+ runInitScript = () => { }
80
84
 
81
85
  setStatus = status => {
82
86
  const id = this.props.tab?.id
@@ -267,7 +271,7 @@ export default class RdpSession extends PureComponent {
267
271
  const kind = e.kind ? e.kind() : 'Unknown'
268
272
  const bt = e.backtrace ? e.backtrace() : ''
269
273
  return `[${kindNames[kind] || kind}] ${bt}`
270
- } catch (_) {}
274
+ } catch (_) { }
271
275
  }
272
276
  return e?.message || e?.toString() || String(e)
273
277
  }
@@ -446,6 +450,14 @@ export default class RdpSession extends PureComponent {
446
450
  this.setState(res, this.handleReInit)
447
451
  }
448
452
 
453
+ handleScaleViewChange = (v) => {
454
+ const scaleViewportId = `rdp-scale-view-${this.props.tab.host}`
455
+ ls.setItemJSON(scaleViewportId, v)
456
+ this.setState({
457
+ scaleViewport: v
458
+ })
459
+ }
460
+
449
461
  renderHelp = () => {
450
462
  return null
451
463
  }
@@ -462,7 +474,7 @@ export default class RdpSession extends PureComponent {
462
474
  onSendCtrlAltDel: this.handleSendCtrlAltDel,
463
475
  screens: [], // RDP doesn't have multi-screen support like VNC
464
476
  currentScreen: null,
465
- onSelectScreen: () => {}, // No-op for RDP
477
+ onSelectScreen: () => { }, // No-op for RDP
466
478
  fixedPosition,
467
479
  showExitFullscreen,
468
480
  className
@@ -517,8 +529,17 @@ export default class RdpSession extends PureComponent {
517
529
  onChange: this.handleResChange,
518
530
  popupMatchSelectWidth: false
519
531
  }
532
+ const scaleProps = {
533
+ checked: this.state.scaleViewport,
534
+ onChange: this.handleScaleViewChange,
535
+ unCheckedChildren: window.translate('scaleViewport'),
536
+ checkedChildren: window.translate('scaleViewport'),
537
+ className: 'mg1l'
538
+ }
520
539
  return (
521
- <div className='pd1 fix session-v-info'>
540
+ <div
541
+ className='pd1 fix session-v-info'
542
+ >
522
543
  <div className='fleft'>
523
544
  <ReloadOutlined
524
545
  onClick={this.handleReInit}
@@ -547,6 +568,9 @@ export default class RdpSession extends PureComponent {
547
568
  />
548
569
  {this.renderInfo()}
549
570
  {this.renderHelp()}
571
+ <Switch
572
+ {...scaleProps}
573
+ />
550
574
  </div>
551
575
  <div className='fright'>
552
576
  {this.props.fullscreenIcon()}
@@ -582,25 +606,34 @@ export default class RdpSession extends PureComponent {
582
606
  height: h + 'px'
583
607
  }
584
608
  }
585
- const { width, height, loading } = this.state
609
+ const { width, height, loading, scaleViewport } = this.state
586
610
  const canvasProps = {
587
611
  width,
588
612
  height,
589
613
  tabIndex: 0
590
614
  }
615
+ if (scaleViewport) {
616
+ Object.assign(canvasProps, {
617
+ style: {
618
+ width: '100%',
619
+ objectFit: 'contain'
620
+ }
621
+ })
622
+ }
623
+ const cls = 'rdp-session-wrap session-v-wrap'
591
624
  const controlProps = this.getControlProps()
592
625
  return (
593
626
  <Spin spinning={loading}>
594
627
  <div
595
628
  {...rdpProps}
596
- className='rdp-session-wrap session-v-wrap'
629
+ className={cls}
597
630
  >
598
631
  {this.renderControl()}
599
- <RemoteFloatControl {...controlProps} />
600
632
  <canvas
601
633
  {...canvasProps}
602
634
  ref={this.canvasRef}
603
635
  />
636
+ <RemoteFloatControl {...controlProps} />
604
637
  </div>
605
638
  </Spin>
606
639
  )
@@ -65,12 +65,13 @@ export default function DeepLinkControl () {
65
65
  }
66
66
 
67
67
  const renderTooltipContent = () => {
68
- const protocols = ['ssh', 'telnet']
68
+ const protocols = ['ssh', 'telnet', 'rdp', 'vnc', 'serial', 'spice', 'electerm']
69
+ const tip = `Register electerm to handle protocol URLs (${protocols.join('://, ')})`
69
70
 
70
71
  return (
71
72
  <div>
72
73
  <div className='pd1b'>
73
- Register electerm to handle protocol URLs (ssh://, telnet://)
74
+ {tip}
74
75
  </div>
75
76
 
76
77
  {registrationStatus && (
@@ -1,4 +1,4 @@
1
- import { PureComponent } from 'react'
1
+ import { PureComponent, createRef } from 'react'
2
2
  import {
3
3
  Button,
4
4
  Input
@@ -19,39 +19,27 @@ export default class ShortcutEdit extends PureComponent {
19
19
  data: null
20
20
  }
21
21
 
22
+ containerRef = createRef()
23
+
24
+ componentWillUnmount () {
25
+ this.removeEventListener()
26
+ }
27
+
22
28
  addEventListener = () => {
23
- const elem = document.querySelector('.ant-drawer')
24
- elem?.addEventListener('click', this.handleClickOuter)
29
+ document.addEventListener('click', this.handleClickOuter, true)
25
30
  document.addEventListener('keydown', this.handleKeyDown)
26
31
  document.addEventListener('mousewheel', this.handleKeyDown)
27
32
  }
28
33
 
29
34
  removeEventListener = () => {
30
- const elem = document.querySelector('.ant-drawer')
31
- elem?.removeEventListener('click', this.handleClickOuter)
35
+ document.removeEventListener('click', this.handleClickOuter, true)
32
36
  document.removeEventListener('keydown', this.handleKeyDown)
33
37
  document.removeEventListener('mousewheel', this.handleKeyDown)
34
38
  }
35
39
 
36
- isInsideElement = (event) => {
37
- const { target } = event
38
- const cls = this.getCls()
39
- if (!target || !target.classList) {
40
- return false
41
- } else if (target.classList.contains(cls)) {
42
- return true
43
- } else {
44
- const parent = target.parentElement
45
- if (parent !== null) {
46
- return this.isInsideElement({ target: parent })
47
- } else {
48
- return false
49
- }
50
- }
51
- }
52
-
53
40
  handleClickOuter = (e) => {
54
- if (!this.isInsideElement(e)) {
41
+ const container = this.containerRef.current
42
+ if (container && !container.contains(e.target)) {
55
43
  this.handleCancel()
56
44
  }
57
45
  }
@@ -78,11 +66,6 @@ export default class ShortcutEdit extends PureComponent {
78
66
  this.handleCancel()
79
67
  }
80
68
 
81
- getCls = () => {
82
- const { index } = this.props.data
83
- return 'shortcut-control-' + index
84
- }
85
-
86
69
  warnCtrolKey = throttle(() => {
87
70
  message.info(
88
71
  'Must have one of Ctrl or Shift or Alt or Meta key',
@@ -131,13 +114,6 @@ export default class ShortcutEdit extends PureComponent {
131
114
  })
132
115
  }
133
116
 
134
- handleClickOutside = () => {
135
- this.removeEventListener()
136
- this.setState({
137
- editMode: false
138
- })
139
- }
140
-
141
117
  renderStatic () {
142
118
  const {
143
119
  shortcut
@@ -199,7 +175,7 @@ export default class ShortcutEdit extends PureComponent {
199
175
  return this.renderStatic()
200
176
  }
201
177
  return (
202
- <div className={this.getCls()}>
178
+ <div ref={this.containerRef}>
203
179
  <Input
204
180
  suffix={this.renderAfter()}
205
181
  value={shortcut}
@@ -6,13 +6,16 @@ import { handleErr } from '../../common/fetch'
6
6
  import {
7
7
  statusMap
8
8
  } from '../../common/constants'
9
- import {
10
- Spin
11
- } from 'antd'
12
9
  import {
13
10
  ReloadOutlined
14
11
  } from '@ant-design/icons'
12
+ import {
13
+ Spin,
14
+ Switch
15
+ } from 'antd'
16
+ import * as ls from '../../common/safe-local-storage'
15
17
  import RemoteFloatControl from '../common/remote-float-control'
18
+ import './spice.styl'
16
19
 
17
20
  async function loadSpiceModule () {
18
21
  if (window.spiceHtml5) return
@@ -25,10 +28,13 @@ async function loadSpiceModule () {
25
28
 
26
29
  export default class SpiceSession extends PureComponent {
27
30
  constructor (props) {
31
+ const scaleViewportId = `spice-scale-view-${props.tab.host}`
32
+ const scaleViewport = ls.getItemJSON(scaleViewportId, false)
28
33
  super(props)
29
34
  this.state = {
30
35
  loading: false,
31
- connected: false
36
+ connected: false,
37
+ scaleViewport
32
38
  }
33
39
  this.spiceConn = null
34
40
  this.screenId = `spice-screen-${props.tab.id}`
@@ -90,6 +96,14 @@ export default class SpiceSession extends PureComponent {
90
96
  }
91
97
  }
92
98
 
99
+ handleScaleViewChange = (v) => {
100
+ const scaleViewportId = `spice-scale-view-${this.props.tab.host}`
101
+ ls.setItemJSON(scaleViewportId, v)
102
+ this.setState({
103
+ scaleViewport: v
104
+ })
105
+ }
106
+
93
107
  getControlProps = (options = {}) => {
94
108
  const {
95
109
  fixedPosition = true,
@@ -115,6 +129,13 @@ export default class SpiceSession extends PureComponent {
115
129
  showExitFullscreen: false,
116
130
  className: 'mg1l'
117
131
  })
132
+ const scaleProps = {
133
+ checked: this.state.scaleViewport,
134
+ onChange: this.handleScaleViewChange,
135
+ unCheckedChildren: window.translate('scaleViewport'),
136
+ checkedChildren: window.translate('scaleViewport'),
137
+ className: 'mg1l'
138
+ }
118
139
  return (
119
140
  <div className='pd1 fix session-v-info'>
120
141
  <div className='fleft'>
@@ -123,6 +144,9 @@ export default class SpiceSession extends PureComponent {
123
144
  className='mg2r mg1l pointer'
124
145
  />
125
146
  {this.renderInfo()}
147
+ <Switch
148
+ {...scaleProps}
149
+ />
126
150
  </div>
127
151
  <div className='fright'>
128
152
  {this.props.fullscreenIcon()}
@@ -236,18 +260,19 @@ export default class SpiceSession extends PureComponent {
236
260
 
237
261
  render () {
238
262
  const { width: w, height: h } = this.props
239
- const { loading } = this.state
263
+ const { loading, scaleViewport } = this.state
240
264
  const { width: innerWidth, height: innerHeight } = this.calcCanvasSize()
241
265
  const wrapperStyle = {
242
266
  width: innerWidth + 'px',
243
267
  height: innerHeight + 'px',
244
- overflow: 'hidden'
268
+ overflow: scaleViewport ? 'hidden' : 'auto'
245
269
  }
270
+ const cls = `spice-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
246
271
  const contrlProps = this.getControlProps()
247
272
  return (
248
273
  <Spin spinning={loading}>
249
274
  <div
250
- className='rdp-session-wrap session-v-wrap'
275
+ className={cls}
251
276
  style={{
252
277
  width: w + 'px',
253
278
  height: h + 'px'
@@ -262,11 +287,6 @@ export default class SpiceSession extends PureComponent {
262
287
  <div
263
288
  ref={this.domRef}
264
289
  id={this.screenId}
265
- className='spice-session-wrap session-v-wrap'
266
- style={{
267
- width: '100%',
268
- height: '100%'
269
- }}
270
290
  />
271
291
  </div>
272
292
  </div>
@@ -0,0 +1,4 @@
1
+ .spice-session-wrap.scale-viewport
2
+ canvas
3
+ width: 100% !important
4
+ object-fit: contain
@@ -28,6 +28,7 @@ export default function TermInteractive () {
28
28
  } else if (
29
29
  e &&
30
30
  e.data &&
31
+ typeof e.data === 'string' &&
31
32
  e.data.includes('ssh-tunnel-result')
32
33
  ) {
33
34
  updateTab(JSON.parse(e.data))
@@ -82,7 +82,7 @@ export default class VncSession extends PureComponent {
82
82
  }
83
83
  }
84
84
 
85
- buildWsUrl = (port, type = 'rdp', extra = '') => {
85
+ buildWsUrl = (port, type = 'vnc', extra = '') => {
86
86
  const { host, tokenElecterm } = this.props.config
87
87
  const { id } = this.props.tab
88
88
  if (window.et.buildWsUrl) {
@@ -56,7 +56,7 @@ export async function addTabFromCommandLine (store, opts) {
56
56
  passphrase: options.passphrase,
57
57
  password: options.password,
58
58
  // port: options.port ? parseInt(options.port, 10) : 22,
59
- type: 'remote',
59
+ type: 'ssh',
60
60
  status: statusMap.processing,
61
61
  id: generate(),
62
62
  encode: encodes[0],
@@ -125,7 +125,7 @@ class Store {
125
125
  const {
126
126
  type
127
127
  } = currentTab
128
- if (type === 'web' || type === 'rdp' || type === 'vnc') {
128
+ if (type === 'web' || type === 'rdp' || type === 'vnc' || type === 'spice') {
129
129
  return false
130
130
  }
131
131
  return currentTab.sshSftpSplitView ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.10.6",
3
+ "version": "2.10.27",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,34 +0,0 @@
1
- import { notification } from './notification'
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
- }