@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.
- package/client/components/ai/ai-config.jsx +26 -3
- package/client/components/ai/ai-history-item.jsx +46 -0
- package/client/components/ai/ai-history.jsx +104 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -4
- package/client/components/bookmark-form/bookmark-schema.js +13 -0
- package/client/components/bookmark-form/config/ftp.js +2 -0
- package/client/components/bookmark-form/config/spice.js +2 -3
- package/client/components/bookmark-form/index.jsx +1 -2
- package/client/components/common/notification.jsx +34 -2
- package/client/components/common/notification.styl +18 -2
- package/client/components/main/wrapper.styl +0 -7
- package/client/components/rdp/rdp-session.jsx +44 -11
- package/client/components/setting-panel/deep-link-control.jsx +3 -2
- package/client/components/shortcuts/shortcut-editor.jsx +12 -36
- package/client/components/spice/spice-session.jsx +32 -12
- package/client/components/spice/spice.styl +4 -0
- package/client/components/terminal/terminal-interactive.jsx +1 -0
- package/client/components/vnc/vnc-session.jsx +1 -1
- package/client/store/load-data.js +1 -1
- package/client/store/store.js +1 -1
- package/package.json +1 -1
- package/client/components/common/notification-with-details.jsx +0 -34
|
@@ -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
|
|
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 &&
|
|
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-
|
|
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
|
|
@@ -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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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>
|
|
@@ -82,7 +82,7 @@ export default class VncSession extends PureComponent {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
buildWsUrl = (port, type = '
|
|
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: '
|
|
59
|
+
type: 'ssh',
|
|
60
60
|
status: statusMap.processing,
|
|
61
61
|
id: generate(),
|
|
62
62
|
encode: encodes[0],
|
package/client/store/store.js
CHANGED
|
@@ -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,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
|
-
}
|