@electerm/electerm-react 2.10.27 → 2.11.6
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/common/default-setting.js +1 -0
- package/client/common/has-active-input.js +1 -1
- package/client/common/parse-quick-connect.js +438 -0
- package/client/components/ai/ai-chat-history-item.jsx +1 -1
- package/client/components/ai/ai-config.jsx +1 -1
- package/client/components/bookmark-form/ai-bookmark-form.jsx +40 -1
- package/client/components/bookmark-form/bookmark-form.styl +21 -0
- package/client/components/bookmark-form/bookmark-from-history-modal.jsx +141 -0
- package/client/components/bookmark-form/tree-select.jsx +72 -23
- package/client/components/common/input-context-menu.jsx +13 -5
- package/client/components/main/main.jsx +3 -0
- package/client/components/rdp/rdp-session.jsx +4 -8
- package/client/components/rdp/rdp.styl +15 -0
- package/client/components/setting-panel/bookmark-tree-list.jsx +1 -0
- package/client/components/setting-panel/list.styl +10 -4
- package/client/components/setting-panel/setting-terminal.jsx +3 -2
- package/client/components/sftp/list-table-ui.jsx +29 -2
- package/client/components/sftp/paged-list.jsx +3 -8
- package/client/components/sidebar/history-item.jsx +13 -1
- package/client/components/sidebar/index.jsx +13 -10
- package/client/components/spice/spice.styl +7 -0
- package/client/components/tabs/add-btn-menu.jsx +2 -0
- package/client/components/tabs/no-session.jsx +25 -9
- package/client/components/tabs/no-session.styl +21 -0
- package/client/components/tabs/quick-connect.jsx +130 -0
- package/client/components/tabs/tabs.styl +1 -19
- package/client/components/terminal/highlight-addon.js +11 -0
- package/client/components/terminal/terminal.jsx +16 -1
- package/client/components/terminal/trzsz-client.js +6 -0
- package/client/components/terminal/xterm-loader.js +11 -0
- package/client/components/terminal-info/run-cmd.jsx +2 -1
- package/client/store/load-data.js +4 -0
- package/client/store/sync.js +1 -0
- package/package.json +1 -1
|
@@ -5,6 +5,6 @@ export default function hasActiveInput (className = 'ant-input-search') {
|
|
|
5
5
|
activeElement.tagName === 'TEXTAREA'
|
|
6
6
|
)
|
|
7
7
|
const hasClass = className ? activeElement.classList.contains(className) : true
|
|
8
|
-
const hasInputDropDown = document.querySelector('.
|
|
8
|
+
const hasInputDropDown = document.querySelector('.input-context-menu')
|
|
9
9
|
return (isInput && hasClass) || hasInputDropDown
|
|
10
10
|
}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick Connect String Parser
|
|
3
|
+
* Parses connection strings according to temp/quick-connect.wiki.md specification
|
|
4
|
+
*
|
|
5
|
+
* Supported Protocols: ssh, telnet, vnc, rdp, spice, serial, ftp, http, https, electerm
|
|
6
|
+
*
|
|
7
|
+
* Basic Format:
|
|
8
|
+
* protocol://[username:password@]host[:port]?anyQueryParam=anyValue&opts={"key":"value"}
|
|
9
|
+
*
|
|
10
|
+
* electerm:// Format (default type is ssh):
|
|
11
|
+
* electerm://[username:password@]host[:port]?type=ssh&anyQueryParam=anyValue
|
|
12
|
+
* electerm://host?type=telnet
|
|
13
|
+
* electerm://user@host:22?type=vnc
|
|
14
|
+
*
|
|
15
|
+
* Shortcut Format (SSH default):
|
|
16
|
+
* user@host
|
|
17
|
+
* user@host:22
|
|
18
|
+
* 192.168.1.100
|
|
19
|
+
* 192.168.1.100:22
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default ports for each protocol
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_PORTS = {
|
|
28
|
+
ssh: 22,
|
|
29
|
+
telnet: 23,
|
|
30
|
+
vnc: 5900,
|
|
31
|
+
rdp: 3389,
|
|
32
|
+
spice: 5900,
|
|
33
|
+
serial: undefined, // Serial doesn't have a default port
|
|
34
|
+
ftp: 21,
|
|
35
|
+
http: 80,
|
|
36
|
+
https: 443,
|
|
37
|
+
electerm: 22 // electerm defaults to SSH port
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default values for each protocol type
|
|
42
|
+
* Based on src/client/components/bookmark-form/config
|
|
43
|
+
*/
|
|
44
|
+
const TYPE_DEFAULT_VALUES = {
|
|
45
|
+
ssh: {
|
|
46
|
+
port: 22,
|
|
47
|
+
enableSsh: true,
|
|
48
|
+
enableSftp: true,
|
|
49
|
+
useSshAgent: true,
|
|
50
|
+
term: 'xterm-256color',
|
|
51
|
+
encode: 'utf-8',
|
|
52
|
+
envLang: 'en_US.UTF-8'
|
|
53
|
+
},
|
|
54
|
+
telnet: {
|
|
55
|
+
port: 23
|
|
56
|
+
},
|
|
57
|
+
vnc: {
|
|
58
|
+
port: 5900,
|
|
59
|
+
viewOnly: false,
|
|
60
|
+
clipViewport: false,
|
|
61
|
+
scaleViewport: true,
|
|
62
|
+
qualityLevel: 3,
|
|
63
|
+
compressionLevel: 1,
|
|
64
|
+
shared: true
|
|
65
|
+
},
|
|
66
|
+
rdp: {
|
|
67
|
+
port: 3389
|
|
68
|
+
},
|
|
69
|
+
spice: {
|
|
70
|
+
port: 5900,
|
|
71
|
+
viewOnly: false,
|
|
72
|
+
scaleViewport: true
|
|
73
|
+
},
|
|
74
|
+
serial: {
|
|
75
|
+
baudRate: 9600,
|
|
76
|
+
dataBits: 8,
|
|
77
|
+
lock: true,
|
|
78
|
+
stopBits: 1,
|
|
79
|
+
parity: 'none',
|
|
80
|
+
rtscts: false,
|
|
81
|
+
xon: false,
|
|
82
|
+
xoff: false,
|
|
83
|
+
xany: false,
|
|
84
|
+
term: 'xterm-256color',
|
|
85
|
+
displayRaw: false
|
|
86
|
+
},
|
|
87
|
+
ftp: {
|
|
88
|
+
port: 21,
|
|
89
|
+
encode: 'utf-8',
|
|
90
|
+
secure: false
|
|
91
|
+
},
|
|
92
|
+
web: {},
|
|
93
|
+
local: {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse a quick connect string into connection options
|
|
98
|
+
* @param {string} str - The connection string
|
|
99
|
+
* @returns {object|null} - Parsed options or null if invalid
|
|
100
|
+
*/
|
|
101
|
+
function parseQuickConnect (str) {
|
|
102
|
+
if (!str || typeof str !== 'string') {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const trimmed = str.trim()
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Detect protocol
|
|
113
|
+
const protocolMatch = trimmed.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
|
|
114
|
+
|
|
115
|
+
let protocol = ''
|
|
116
|
+
let connectionString = ''
|
|
117
|
+
let originalProtocol = 'ssh'
|
|
118
|
+
|
|
119
|
+
if (protocolMatch) {
|
|
120
|
+
originalProtocol = protocolMatch[1].toLowerCase()
|
|
121
|
+
protocol = originalProtocol
|
|
122
|
+
// Normalize http/https to web
|
|
123
|
+
if (protocol === 'http' || protocol === 'https') {
|
|
124
|
+
protocol = 'web'
|
|
125
|
+
}
|
|
126
|
+
connectionString = trimmed.slice(protocolMatch[0].length)
|
|
127
|
+
} else {
|
|
128
|
+
// Shortcut format - default to SSH
|
|
129
|
+
// Match user@host or user@host:port or just host or host:port
|
|
130
|
+
// Use last colon to determine port for host:port format
|
|
131
|
+
if (/^[\w.-]+@[\w.-]+/.test(trimmed)) {
|
|
132
|
+
// user@host or user@host:port
|
|
133
|
+
protocol = 'ssh'
|
|
134
|
+
connectionString = trimmed
|
|
135
|
+
} else if (/^[\w.-]+:.*:[\d]+$/.test(trimmed)) {
|
|
136
|
+
// host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344)
|
|
137
|
+
// Check if the last colon is followed by digits (port number)
|
|
138
|
+
protocol = 'ssh'
|
|
139
|
+
connectionString = trimmed
|
|
140
|
+
} else if (/^[\w.-]+:[\d]+$/.test(trimmed)) {
|
|
141
|
+
// host:port (no username, simple format like host:22)
|
|
142
|
+
protocol = 'ssh'
|
|
143
|
+
connectionString = trimmed
|
|
144
|
+
} else if (/^[\w.-]+$/.test(trimmed)) {
|
|
145
|
+
// just host
|
|
146
|
+
protocol = 'ssh'
|
|
147
|
+
connectionString = trimmed
|
|
148
|
+
} else {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!SUPPORTED_PROTOCOLS.includes(protocol) && protocol !== 'web') {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Extract opts from the connection string before parsing
|
|
158
|
+
let optsStr = ''
|
|
159
|
+
const optsMatch = connectionString.match(/[?&]opts=('|")(.+?)('|")$/)
|
|
160
|
+
if (!optsMatch) {
|
|
161
|
+
// Try without quotes
|
|
162
|
+
const optsMatchNoQuote = connectionString.match(/[?&]opts=(\{.+?\})$/)
|
|
163
|
+
if (optsMatchNoQuote) {
|
|
164
|
+
optsStr = optsMatchNoQuote[1]
|
|
165
|
+
connectionString = connectionString.slice(0, optsMatchNoQuote.index)
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
optsStr = optsMatch[2]
|
|
169
|
+
connectionString = connectionString.slice(0, optsMatch.index)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Extract query string for web type and electerm type
|
|
173
|
+
let queryStr = ''
|
|
174
|
+
const queryMatch = connectionString.match(/\?(.+)$/)
|
|
175
|
+
if (queryMatch) {
|
|
176
|
+
queryStr = queryMatch[1]
|
|
177
|
+
connectionString = connectionString.slice(0, queryMatch.index)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Parse username:password@host:port
|
|
181
|
+
// First, check if there's an @ for auth
|
|
182
|
+
let username = ''
|
|
183
|
+
let password = ''
|
|
184
|
+
let hostOrPath = ''
|
|
185
|
+
let port = ''
|
|
186
|
+
|
|
187
|
+
const atIndex = connectionString.indexOf('@')
|
|
188
|
+
if (atIndex !== -1) {
|
|
189
|
+
// Has auth
|
|
190
|
+
const authPart = connectionString.slice(0, atIndex)
|
|
191
|
+
const hostPart = connectionString.slice(atIndex + 1)
|
|
192
|
+
const colonIndex = authPart.indexOf(':')
|
|
193
|
+
if (colonIndex !== -1) {
|
|
194
|
+
username = authPart.slice(0, colonIndex)
|
|
195
|
+
password = authPart.slice(colonIndex + 1)
|
|
196
|
+
} else {
|
|
197
|
+
username = authPart
|
|
198
|
+
}
|
|
199
|
+
// Parse host:port from hostPart
|
|
200
|
+
const hostColonIndex = hostPart.lastIndexOf(':')
|
|
201
|
+
if (hostColonIndex !== -1) {
|
|
202
|
+
hostOrPath = hostPart.slice(0, hostColonIndex)
|
|
203
|
+
port = hostPart.slice(hostColonIndex + 1)
|
|
204
|
+
} else {
|
|
205
|
+
hostOrPath = hostPart
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// No @ sign - check for special case: protocol://password:host (e.g., spice://password:host)
|
|
209
|
+
// This only applies to spice protocol
|
|
210
|
+
if (protocol === 'spice') {
|
|
211
|
+
// Count colons in the connection string
|
|
212
|
+
const colonCount = (connectionString.match(/:/g) || []).length
|
|
213
|
+
|
|
214
|
+
if (colonCount >= 2) {
|
|
215
|
+
// Multiple colons - could be password:host:port or host:port with IP
|
|
216
|
+
// Use lastIndexOf for port, then check if first part is password or IP
|
|
217
|
+
const lastColonIndex = connectionString.lastIndexOf(':')
|
|
218
|
+
const portCandidate = connectionString.slice(lastColonIndex + 1)
|
|
219
|
+
|
|
220
|
+
if (/^\d+$/.test(portCandidate)) {
|
|
221
|
+
// Last part is a port number
|
|
222
|
+
const hostPortPart = connectionString.slice(0, lastColonIndex)
|
|
223
|
+
const secondLastColonIndex = hostPortPart.lastIndexOf(':')
|
|
224
|
+
|
|
225
|
+
if (secondLastColonIndex !== -1) {
|
|
226
|
+
// There's another colon - first part could be password
|
|
227
|
+
const potentialPassword = hostPortPart.slice(0, secondLastColonIndex)
|
|
228
|
+
const hostPart = hostPortPart.slice(secondLastColonIndex + 1)
|
|
229
|
+
|
|
230
|
+
// Check if potentialPassword is NOT an IP/hostname
|
|
231
|
+
// An IP/hostname should contain dots, a password typically doesn't
|
|
232
|
+
// Also check it's not a simple number (port)
|
|
233
|
+
const isIPorHostname = (potentialPassword.includes('.') || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(potentialPassword))
|
|
234
|
+
|
|
235
|
+
if (isIPorHostname) {
|
|
236
|
+
// It's IP, no password
|
|
237
|
+
hostOrPath = hostPortPart
|
|
238
|
+
port = portCandidate
|
|
239
|
+
} else {
|
|
240
|
+
// It's password
|
|
241
|
+
password = potentialPassword
|
|
242
|
+
hostOrPath = hostPart
|
|
243
|
+
port = portCandidate
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// Only one colon before the port - it's host:port
|
|
247
|
+
hostOrPath = hostPortPart
|
|
248
|
+
port = portCandidate
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// Last part is not a port
|
|
252
|
+
hostOrPath = connectionString
|
|
253
|
+
}
|
|
254
|
+
} else if (colonCount === 1) {
|
|
255
|
+
// Single colon - could be host:port or just a word with colon
|
|
256
|
+
const colonIndex = connectionString.indexOf(':')
|
|
257
|
+
const firstPart = connectionString.slice(0, colonIndex)
|
|
258
|
+
const secondPart = connectionString.slice(colonIndex + 1)
|
|
259
|
+
|
|
260
|
+
// Check if first part is an IP
|
|
261
|
+
const isIP = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(firstPart)
|
|
262
|
+
|
|
263
|
+
if (isIP) {
|
|
264
|
+
// IP with port
|
|
265
|
+
hostOrPath = firstPart
|
|
266
|
+
port = secondPart
|
|
267
|
+
} else if (/^\d+$/.test(secondPart)) {
|
|
268
|
+
// Just a word with port number
|
|
269
|
+
// This is likely password (for spice) or host without port
|
|
270
|
+
// For spice, treat first part as host (not password) since there's only one colon
|
|
271
|
+
hostOrPath = firstPart
|
|
272
|
+
port = secondPart
|
|
273
|
+
} else {
|
|
274
|
+
// host or hostname
|
|
275
|
+
hostOrPath = connectionString
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// No colon - just host
|
|
279
|
+
hostOrPath = connectionString
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
// Normal case - just host:port
|
|
283
|
+
const hostColonIndex = connectionString.lastIndexOf(':')
|
|
284
|
+
if (hostColonIndex !== -1) {
|
|
285
|
+
// Make sure it's a port number (all digits)
|
|
286
|
+
const potentialPort = connectionString.slice(hostColonIndex + 1)
|
|
287
|
+
if (/^\d+$/.test(potentialPort)) {
|
|
288
|
+
hostOrPath = connectionString.slice(0, hostColonIndex)
|
|
289
|
+
port = potentialPort
|
|
290
|
+
} else {
|
|
291
|
+
hostOrPath = connectionString
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
hostOrPath = connectionString
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!hostOrPath) {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build base options
|
|
304
|
+
// For electerm protocol, we need to handle the type from query params
|
|
305
|
+
let finalProtocol = protocol
|
|
306
|
+
let webProtocol = originalProtocol // Store original for web type
|
|
307
|
+
|
|
308
|
+
// Handle electerm:// protocol - extract type from query params, default to ssh
|
|
309
|
+
if (originalProtocol === 'electerm') {
|
|
310
|
+
// Parse query params to get type
|
|
311
|
+
const params = new URLSearchParams(queryStr)
|
|
312
|
+
finalProtocol = params.get('type') || params.get('tp') || 'ssh'
|
|
313
|
+
|
|
314
|
+
// Validate the type is supported
|
|
315
|
+
if (!SUPPORTED_PROTOCOLS.includes(finalProtocol) && finalProtocol !== 'web') {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Normalize http/https to web
|
|
320
|
+
if (finalProtocol === 'http' || finalProtocol === 'https') {
|
|
321
|
+
webProtocol = finalProtocol // Store the http/https before normalizing
|
|
322
|
+
finalProtocol = 'web'
|
|
323
|
+
// Remove type/tp from query string for web URL construction
|
|
324
|
+
params.delete('type')
|
|
325
|
+
params.delete('tp')
|
|
326
|
+
queryStr = params.toString()
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
webProtocol = originalProtocol
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const opts = {
|
|
333
|
+
type: finalProtocol
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle different protocol types
|
|
337
|
+
if (finalProtocol === 'serial') {
|
|
338
|
+
// Serial: path is the port
|
|
339
|
+
opts.path = hostOrPath
|
|
340
|
+
if (port) {
|
|
341
|
+
opts.baudRate = parseInt(port, 10)
|
|
342
|
+
}
|
|
343
|
+
// Parse query params for serial (like baudRate)
|
|
344
|
+
if (queryStr) {
|
|
345
|
+
const params = new URLSearchParams(queryStr)
|
|
346
|
+
if (params.has('baudRate')) {
|
|
347
|
+
opts.baudRate = parseInt(params.get('baudRate'), 10)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else if (finalProtocol === 'web') {
|
|
351
|
+
// Web: construct URL from protocol + host + port + query
|
|
352
|
+
let url = `${webProtocol}://${hostOrPath}`
|
|
353
|
+
if (port) {
|
|
354
|
+
// Add non-standard port to URL
|
|
355
|
+
const defaultPort = originalProtocol === 'https' ? 443 : 80
|
|
356
|
+
if (parseInt(port, 10) !== defaultPort) {
|
|
357
|
+
url += `:${port}`
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Add query string if present
|
|
361
|
+
if (queryStr) {
|
|
362
|
+
const separator = url.includes('?') ? '&' : '?'
|
|
363
|
+
url += `${separator}${queryStr}`
|
|
364
|
+
}
|
|
365
|
+
opts.url = url
|
|
366
|
+
} else {
|
|
367
|
+
// SSH, Telnet, VNC, RDP, Spice, FTP
|
|
368
|
+
opts.host = hostOrPath
|
|
369
|
+
if (port) {
|
|
370
|
+
opts.port = parseInt(port, 10)
|
|
371
|
+
}
|
|
372
|
+
if (username !== undefined && username !== '') {
|
|
373
|
+
opts.username = username
|
|
374
|
+
}
|
|
375
|
+
if (password !== undefined && password !== '') {
|
|
376
|
+
opts.password = password
|
|
377
|
+
}
|
|
378
|
+
// Parse query params for other protocols (like title)
|
|
379
|
+
if (queryStr) {
|
|
380
|
+
const params = new URLSearchParams(queryStr)
|
|
381
|
+
if (params.has('title')) {
|
|
382
|
+
opts.title = params.get('title')
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Parse opts JSON to extend params
|
|
388
|
+
if (optsStr) {
|
|
389
|
+
try {
|
|
390
|
+
const extraOpts = JSON.parse(optsStr)
|
|
391
|
+
Object.assign(opts, extraOpts)
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error('Failed to parse opts:', err)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Apply default values for the protocol type
|
|
398
|
+
const typeDefaults = TYPE_DEFAULT_VALUES[finalProtocol]
|
|
399
|
+
if (typeDefaults) {
|
|
400
|
+
Object.keys(typeDefaults).forEach(key => {
|
|
401
|
+
// Only apply default if not already set
|
|
402
|
+
if (opts[key] === undefined) {
|
|
403
|
+
opts[key] = typeDefaults[key]
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return opts
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Error parsing quick connect string:', error)
|
|
411
|
+
return null
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get default port for a protocol
|
|
417
|
+
* @param {string} protocol - The protocol name
|
|
418
|
+
* @returns {number|undefined} - Default port or undefined
|
|
419
|
+
*/
|
|
420
|
+
function getDefaultPort (protocol) {
|
|
421
|
+
return DEFAULT_PORTS[protocol]
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get list of supported protocols
|
|
426
|
+
* @returns {string[]} - List of supported protocols
|
|
427
|
+
*/
|
|
428
|
+
function getSupportedProtocols () {
|
|
429
|
+
return [...SUPPORTED_PROTOCOLS]
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export {
|
|
433
|
+
parseQuickConnect,
|
|
434
|
+
getDefaultPort,
|
|
435
|
+
getSupportedProtocols,
|
|
436
|
+
SUPPORTED_PROTOCOLS,
|
|
437
|
+
DEFAULT_PORTS
|
|
438
|
+
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
EditOutlined,
|
|
12
12
|
CopyOutlined,
|
|
13
13
|
DownloadOutlined,
|
|
14
|
-
EyeOutlined
|
|
14
|
+
EyeOutlined,
|
|
15
|
+
ThunderboltOutlined
|
|
15
16
|
} from '@ant-design/icons'
|
|
16
17
|
import SimpleEditor from '../text-editor/simple-editor'
|
|
17
18
|
import { copy } from '../../common/clipboard'
|
|
@@ -24,6 +25,7 @@ import { fixBookmarkData } from './fix-bookmark-default.js'
|
|
|
24
25
|
import generate from '../../common/id-with-stamp'
|
|
25
26
|
import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
|
|
26
27
|
import { getItem, setItem } from '../../common/safe-local-storage'
|
|
28
|
+
import newTerminal from '../../common/new-terminal'
|
|
27
29
|
|
|
28
30
|
const STORAGE_KEY_DESC = 'ai_bookmark_description'
|
|
29
31
|
const STORAGE_KEY_HISTORY = 'ai_bookmark_history'
|
|
@@ -166,6 +168,30 @@ export default function AIBookmarkForm (props) {
|
|
|
166
168
|
setShowConfirm(false)
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
const handleQuickConnect = () => {
|
|
172
|
+
const parsed = getGeneratedData()
|
|
173
|
+
if (!parsed.length || !parsed[0]) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
const bm = parsed[0]
|
|
177
|
+
// Create a new tab with quick connect options
|
|
178
|
+
const { store } = window
|
|
179
|
+
|
|
180
|
+
// Close the setting panel first
|
|
181
|
+
store.hideSettingModal()
|
|
182
|
+
|
|
183
|
+
const tabOptions = {
|
|
184
|
+
...bm,
|
|
185
|
+
...newTerminal(),
|
|
186
|
+
from: 'quickConnect'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
store.addTab(tabOptions)
|
|
190
|
+
setShowConfirm(false)
|
|
191
|
+
setDescription('')
|
|
192
|
+
message.success(e('Done'))
|
|
193
|
+
}
|
|
194
|
+
|
|
169
195
|
const handleCancel = () => {
|
|
170
196
|
if (onCancel) {
|
|
171
197
|
onCancel()
|
|
@@ -245,6 +271,18 @@ export default function AIBookmarkForm (props) {
|
|
|
245
271
|
loading
|
|
246
272
|
}
|
|
247
273
|
|
|
274
|
+
function renderQuickConnectBtn () {
|
|
275
|
+
const parsed = getGeneratedData()
|
|
276
|
+
if (!parsed.length || !parsed[0] || parsed.length > 1) {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
return (
|
|
280
|
+
<Button onClick={handleQuickConnect} icon={<ThunderboltOutlined />}>
|
|
281
|
+
{e('quickConnect')}
|
|
282
|
+
</Button>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
248
286
|
const modalProps = {
|
|
249
287
|
title: e('confirmBookmarkData') || 'Confirm Bookmark Data',
|
|
250
288
|
open: showConfirm,
|
|
@@ -254,6 +292,7 @@ export default function AIBookmarkForm (props) {
|
|
|
254
292
|
<Button onClick={handleCancelConfirm}>
|
|
255
293
|
<CloseOutlined /> {e('cancel')}
|
|
256
294
|
</Button>
|
|
295
|
+
{renderQuickConnectBtn()}
|
|
257
296
|
<Button type='primary' onClick={handleConfirm}>
|
|
258
297
|
<CheckOutlined /> {e('confirm')}
|
|
259
298
|
</Button>
|
|
@@ -15,3 +15,24 @@
|
|
|
15
15
|
display inline-block
|
|
16
16
|
.number-input
|
|
17
17
|
min-width 210px
|
|
18
|
+
|
|
19
|
+
.tree-select-wrapper
|
|
20
|
+
display flex
|
|
21
|
+
flex-direction column
|
|
22
|
+
height 100%
|
|
23
|
+
overflow hidden
|
|
24
|
+
|
|
25
|
+
.tree-select-header
|
|
26
|
+
flex-shrink 0
|
|
27
|
+
|
|
28
|
+
.tree-select-content
|
|
29
|
+
flex 1
|
|
30
|
+
overflow auto
|
|
31
|
+
margin-top 8px
|
|
32
|
+
|
|
33
|
+
.bookmark-json-preview
|
|
34
|
+
padding 12px
|
|
35
|
+
max-height 300px
|
|
36
|
+
overflow auto
|
|
37
|
+
font-size 12px
|
|
38
|
+
font-family monospace
|