@electerm/electerm-react 5.1.25 → 5.3.0
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/build-map.js +12 -0
- package/client/common/constants.js +12 -0
- package/client/common/db.js +7 -5
- package/client/common/import-task.js +174 -0
- package/client/common/term-of-use.jsx +69 -0
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
- package/client/components/bookmark-form/common/fields.jsx +1 -1
- package/client/components/bookmark-form/common/profile-item.jsx +12 -2
- package/client/components/bookmark-form/config/common-fields.js +11 -0
- package/client/components/bookmark-form/config/rdp.js +1 -1
- package/client/components/bookmark-form/tree-select.jsx +5 -16
- package/client/components/common/import-progress.jsx +90 -0
- package/client/components/footer/footer-entry.jsx +1 -1
- package/client/components/main/main.jsx +2 -0
- package/client/components/profile/profile-form-rdp.jsx +1 -1
- package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
- package/client/components/rdp/rdp-cred-form.jsx +55 -0
- package/client/components/rdp/rdp-session.jsx +97 -6
- package/client/components/setting-panel/keywords-transport.jsx +1 -4
- package/client/components/setting-panel/setting-modal.jsx +8 -2
- package/client/components/setting-panel/start-session-select.jsx +5 -16
- package/client/components/setting-sync/setting-sync.jsx +15 -7
- package/client/components/sidebar/info-modal.jsx +7 -0
- package/client/components/spice/spice-session.jsx +40 -0
- package/client/components/terminal/attach-addon-custom.js +12 -10
- package/client/components/terminal/shell.js +22 -11
- package/client/components/terminal/shortcut-bar-entry.jsx +52 -1
- package/client/components/terminal/terminal-color-query.mjs +32 -27
- package/client/components/terminal/terminal-interactive.jsx +35 -0
- package/client/components/terminal/terminal.jsx +24 -15
- package/client/components/terminal-info/activity.jsx +5 -1
- package/client/components/terminal-info/base.jsx +37 -22
- package/client/components/terminal-info/disk.jsx +5 -1
- package/client/components/terminal-info/local-info-resolver.js +89 -0
- package/client/components/terminal-info/network.jsx +5 -1
- package/client/components/terminal-info/run-cmd.jsx +42 -3
- package/client/components/terminal-info/sys-info.jsx +56 -0
- package/client/components/terminal-info/terminal-info.jsx +4 -1
- package/client/components/terminal-info/terminal-info.styl +22 -7
- package/client/components/tree-list/bookmark-transport.jsx +2 -1
- package/client/components/tree-list/bookmark-upload.js +56 -41
- package/client/components/tree-list/tree-list-row.jsx +11 -4
- package/client/components/tree-list/tree-list-rows.js +6 -3
- package/client/components/tree-list/tree-list.jsx +4 -2
- package/client/store/common.js +38 -3
- package/client/store/load-data.js +6 -1
- package/client/store/setting.js +7 -4
- package/client/store/store.js +3 -12
- package/client/store/sync.js +31 -2
- package/client/store/tab.js +16 -11
- package/client/store/watch.js +10 -1
- package/package.json +1 -1
|
@@ -212,6 +212,16 @@ export const syncTypes = buildConst([
|
|
|
212
212
|
'cloud',
|
|
213
213
|
'webdav'
|
|
214
214
|
])
|
|
215
|
+
export const allowedSyncTypes = () => {
|
|
216
|
+
const custom = window.et.syncTypes
|
|
217
|
+
if (!Array.isArray(custom)) {
|
|
218
|
+
return Object.keys(syncTypes)
|
|
219
|
+
}
|
|
220
|
+
const list = custom.filter(type => syncTypes[type])
|
|
221
|
+
return list.length
|
|
222
|
+
? list
|
|
223
|
+
: Object.keys(syncTypes)
|
|
224
|
+
}
|
|
215
225
|
export const syncTokenCreateUrls = {
|
|
216
226
|
gitee: 'https://gitee.com/github-zxdong262/electerm/wikis/Create%20personal%20access%20token?sort_id=3028409',
|
|
217
227
|
github: 'https://github.com/electerm/electerm/wiki/Create-personal-access-token',
|
|
@@ -262,6 +272,8 @@ export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/w
|
|
|
262
272
|
export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
|
|
263
273
|
export const aiChatModeLsKey = 'ai-chat-mode'
|
|
264
274
|
export const lastAiChatSessionIdKey = 'last-ai-chat-session-id'
|
|
275
|
+
export const aiTermOfUseConfirmedLsKey = 'ai-term-of-use-confirmed'
|
|
276
|
+
export const syncTermOfUseConfirmedLsKey = 'sync-term-of-use-confirmed'
|
|
265
277
|
export const modals = {
|
|
266
278
|
hide: 0,
|
|
267
279
|
setting: 1
|
package/client/common/db.js
CHANGED
|
@@ -161,11 +161,13 @@ export async function fetchInitData (dbName) {
|
|
|
161
161
|
const res = await find(dbName)
|
|
162
162
|
const order = await getData(`${dbName}:order`)
|
|
163
163
|
if (order && order.length) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
// index map instead of findIndex inside the comparator:
|
|
165
|
+
// comparator sort is O(n²) with findIndex, O(n log n) with a map
|
|
166
|
+
const orderMap = new Map(order.map((id, i) => [id, i]))
|
|
167
|
+
res.sort((a, b) =>
|
|
168
|
+
(orderMap.get(a.id) ?? -1) -
|
|
169
|
+
(orderMap.get(b.id) ?? -1)
|
|
170
|
+
)
|
|
169
171
|
}
|
|
170
172
|
return JSON.stringify(res)
|
|
171
173
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chunked import task runner (no UI).
|
|
3
|
+
*
|
|
4
|
+
* Runs large data imports (bookmarks, quick commands, all-data) in small
|
|
5
|
+
* batches, yielding to the UI between batches and pausing db watchers while
|
|
6
|
+
* running, so a big import can not freeze or crash the app. Progress is
|
|
7
|
+
* reported through the ImportProgress modal mounted in main.jsx (driven via
|
|
8
|
+
* refsStatic) and through the onProgress callback.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { action } from 'manate'
|
|
12
|
+
import { refsStatic } from '../components/common/ref'
|
|
13
|
+
|
|
14
|
+
export const importProgressRefKey = 'import-progress'
|
|
15
|
+
|
|
16
|
+
function yieldToUI (idleMs = 0) {
|
|
17
|
+
// rAF fires *before* paint; pairing it with setTimeout(0) - a macrotask
|
|
18
|
+
// that runs *after* paint - guarantees the browser actually paints
|
|
19
|
+
// between batches instead of coalescing them into one long frame.
|
|
20
|
+
// idleMs adds a real idle gap after paint so input events, hover and
|
|
21
|
+
// the mouse cursor get a chance to be handled between batches
|
|
22
|
+
return new Promise(resolve => {
|
|
23
|
+
const finish = () => {
|
|
24
|
+
if (idleMs > 0) {
|
|
25
|
+
setTimeout(resolve, idleMs)
|
|
26
|
+
} else {
|
|
27
|
+
resolve()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
31
|
+
requestAnimationFrame(() => setTimeout(finish, 0))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
setTimeout(finish, 0)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let controller = null
|
|
39
|
+
|
|
40
|
+
export function cancelImportTask () {
|
|
41
|
+
if (controller) {
|
|
42
|
+
controller.cancelled = true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run an import task in UI-friendly batches with a progress modal.
|
|
48
|
+
* @param {Object} options
|
|
49
|
+
* @param {string} options.title modal title (already translated label)
|
|
50
|
+
* @param {Array} options.steps list of steps, each either
|
|
51
|
+
* `{ label, items, process(chunk, step) }` - chunked store mutations, or
|
|
52
|
+
* `{ label, weight, run(step) }` - a single (async) unit of work
|
|
53
|
+
* @param {number} options.batch items per store-mutation batch
|
|
54
|
+
* @param {number} options.batchIdle ms of idle time after each batch's
|
|
55
|
+
* paint (default 16); keeps the UI responsive to input while importing
|
|
56
|
+
* @param {boolean} options.cancelable show a cancel button (default true)
|
|
57
|
+
* @param {boolean} options.useModal show the progress modal (default true)
|
|
58
|
+
* @param {number} options.modalMinTotal skip the modal below this total (default 200)
|
|
59
|
+
* @param {string[]} options.stopWatchers db watcher names (window.watchXxx)
|
|
60
|
+
* to pause during the task, so chunked writes do not trigger a db diff
|
|
61
|
+
* per batch; watchers restart one per frame after the steps finish
|
|
62
|
+
* @param {Function} options.onProgress called as (current, total) per batch
|
|
63
|
+
* @returns {Promise<{cancelled: boolean, error: Error|null}>}
|
|
64
|
+
*/
|
|
65
|
+
export async function runImportTask (options) {
|
|
66
|
+
const {
|
|
67
|
+
title = '',
|
|
68
|
+
steps = [],
|
|
69
|
+
batch = 100,
|
|
70
|
+
batchIdle = 16,
|
|
71
|
+
cancelable = true,
|
|
72
|
+
useModal = true,
|
|
73
|
+
modalMinTotal = 200,
|
|
74
|
+
stopWatchers = [],
|
|
75
|
+
onProgress
|
|
76
|
+
} = options
|
|
77
|
+
if (controller) {
|
|
78
|
+
console.warn('Another import task is already running')
|
|
79
|
+
return { cancelled: false, error: new Error('Another import task is already running') }
|
|
80
|
+
}
|
|
81
|
+
const total = steps.reduce((p, s) => {
|
|
82
|
+
return p + (Array.isArray(s.items) ? s.items.length : (s.weight || 1))
|
|
83
|
+
}, 0) + stopWatchers.length
|
|
84
|
+
const showModal = useModal && total >= modalMinTotal
|
|
85
|
+
const modal = refsStatic.get(importProgressRefKey)
|
|
86
|
+
controller = { cancelled: false }
|
|
87
|
+
let done = 0
|
|
88
|
+
const update = (ext) => {
|
|
89
|
+
if (onProgress) {
|
|
90
|
+
onProgress(done, total)
|
|
91
|
+
}
|
|
92
|
+
if (!showModal || !modal) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
modal.show({
|
|
96
|
+
title,
|
|
97
|
+
status: 'running',
|
|
98
|
+
current: done,
|
|
99
|
+
total,
|
|
100
|
+
label: '',
|
|
101
|
+
cancelable,
|
|
102
|
+
...ext
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
update({})
|
|
106
|
+
for (const name of stopWatchers) {
|
|
107
|
+
window[`watch${name}`]?.stop()
|
|
108
|
+
}
|
|
109
|
+
let error = null
|
|
110
|
+
const restarted = new Set()
|
|
111
|
+
try {
|
|
112
|
+
for (const step of steps) {
|
|
113
|
+
if (controller.cancelled) {
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
const { label, items, process, run } = step
|
|
117
|
+
if (Array.isArray(items) && typeof process === 'function') {
|
|
118
|
+
for (let i = 0; i < items.length; i += batch) {
|
|
119
|
+
if (controller.cancelled) {
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
// paint the last batch, then leave a real idle gap so the UI
|
|
123
|
+
// can handle input/cursor events before the next batch
|
|
124
|
+
await yieldToUI(batchIdle)
|
|
125
|
+
const chunk = items.slice(i, i + batch)
|
|
126
|
+
action(() => process(chunk, step))()
|
|
127
|
+
done += chunk.length
|
|
128
|
+
update({ label })
|
|
129
|
+
}
|
|
130
|
+
} else if (typeof run === 'function') {
|
|
131
|
+
await yieldToUI()
|
|
132
|
+
await run(step)
|
|
133
|
+
done += step.weight || 1
|
|
134
|
+
update({ label })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// restart watchers one per frame - each start() synchronously diffs
|
|
138
|
+
// + deep-copies its collection and writes to db, so restarting all at
|
|
139
|
+
// once would freeze the UI right when the import "finishes"
|
|
140
|
+
for (const name of stopWatchers) {
|
|
141
|
+
await yieldToUI(batchIdle)
|
|
142
|
+
window[`watch${name}`]?.start()
|
|
143
|
+
restarted.add(name)
|
|
144
|
+
done += 1
|
|
145
|
+
update({ label: name })
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
error = err
|
|
149
|
+
} finally {
|
|
150
|
+
// on error/cancel still make sure everything is restarted
|
|
151
|
+
for (const name of stopWatchers) {
|
|
152
|
+
if (!restarted.has(name)) {
|
|
153
|
+
window[`watch${name}`]?.start()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const { cancelled } = controller
|
|
158
|
+
controller = null
|
|
159
|
+
if (error) {
|
|
160
|
+
if (showModal && modal) {
|
|
161
|
+
modal.show({ title, status: 'error', error, current: done, total, cancelable: false })
|
|
162
|
+
}
|
|
163
|
+
} else if (!cancelled) {
|
|
164
|
+
if (showModal && modal) {
|
|
165
|
+
modal.show({ title, status: 'done', current: total, total, cancelable: false })
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
modal.hide()
|
|
168
|
+
}, 600)
|
|
169
|
+
}
|
|
170
|
+
} else if (showModal && modal) {
|
|
171
|
+
modal.hide()
|
|
172
|
+
}
|
|
173
|
+
return { cancelled, error }
|
|
174
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term of use confirmation gate for the AI config / data sync pages.
|
|
3
|
+
*
|
|
4
|
+
* Define `window.et.AITermOfUse` / `window.et.SyncTermOfUse` (injected the
|
|
5
|
+
* same way as `defaultAIPreset` into the built index.html `_global` data)
|
|
6
|
+
* to require users to confirm the term before entering those pages.
|
|
7
|
+
* Confirmation is saved to localStorage, so it is only asked once.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Modal from '../components/common/modal'
|
|
11
|
+
import {
|
|
12
|
+
aiTermOfUseConfirmedLsKey,
|
|
13
|
+
syncTermOfUseConfirmedLsKey
|
|
14
|
+
} from './constants'
|
|
15
|
+
import * as ls from './safe-local-storage'
|
|
16
|
+
|
|
17
|
+
const e = window.translate
|
|
18
|
+
|
|
19
|
+
const termOfUseTypes = {
|
|
20
|
+
ai: {
|
|
21
|
+
etKey: 'AITermOfUse',
|
|
22
|
+
lsKey: aiTermOfUseConfirmedLsKey
|
|
23
|
+
},
|
|
24
|
+
sync: {
|
|
25
|
+
etKey: 'SyncTermOfUse',
|
|
26
|
+
lsKey: syncTermOfUseConfirmedLsKey
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isConfirmed (type) {
|
|
31
|
+
return ls.getItem(termOfUseTypes[type].lsKey) === 'yes'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run `onConfirmed` after the term of use of `type` is confirmed.
|
|
36
|
+
* When no term is defined, or it was confirmed before, `onConfirmed`
|
|
37
|
+
* runs synchronously; otherwise a confirmation modal is shown and
|
|
38
|
+
* `onConfirmed` only runs when the user confirms it.
|
|
39
|
+
*/
|
|
40
|
+
export function requireTermOfUse (type, onConfirmed) {
|
|
41
|
+
const { etKey, lsKey } = termOfUseTypes[type]
|
|
42
|
+
const content = window.et?.[etKey]
|
|
43
|
+
const text = typeof content === 'string' ? content.trim() : ''
|
|
44
|
+
if (!text || isConfirmed(type)) {
|
|
45
|
+
return onConfirmed()
|
|
46
|
+
}
|
|
47
|
+
Modal.confirm({
|
|
48
|
+
title: 'Terms of use',
|
|
49
|
+
width: 600,
|
|
50
|
+
maskClosable: false,
|
|
51
|
+
content: (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
maxHeight: '50vh',
|
|
55
|
+
overflow: 'auto',
|
|
56
|
+
whiteSpace: 'pre-wrap'
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{text}
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
okText: e('haveRead'),
|
|
63
|
+
cancelText: e('cancel'),
|
|
64
|
+
onOk: () => {
|
|
65
|
+
ls.setItem(lsKey, 'yes')
|
|
66
|
+
onConfirmed()
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -27,6 +27,14 @@ export const defaultAIPresets = [
|
|
|
27
27
|
modelAI: 'DEEPSEEK_V4_FLASH',
|
|
28
28
|
authHeaderNameAI: 'Authorization: Bearer'
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'apimart',
|
|
32
|
+
nameAI: 'ApiMart',
|
|
33
|
+
baseURLAI: 'https://api.apimart.ai/v1',
|
|
34
|
+
apiPathAI: '/chat/completions',
|
|
35
|
+
modelAI: 'deepseek-v4-flash',
|
|
36
|
+
authHeaderNameAI: 'Authorization: Bearer'
|
|
37
|
+
},
|
|
30
38
|
{
|
|
31
39
|
id: 'deepseek',
|
|
32
40
|
nameAI: 'DeepSeek',
|
|
@@ -27,6 +27,7 @@ import { fixBookmarkData } from './fix-bookmark-default.js'
|
|
|
27
27
|
import generate from '../../common/id-with-stamp'
|
|
28
28
|
import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
|
|
29
29
|
import { getItem, setItem } from '../../common/safe-local-storage'
|
|
30
|
+
import { runImportTask } from '../../common/import-task'
|
|
30
31
|
|
|
31
32
|
const STORAGE_KEY_DESC = 'ai_bookmark_description'
|
|
32
33
|
const STORAGE_KEY_HISTORY = 'ai_bookmark_history'
|
|
@@ -34,16 +35,6 @@ const EVENT_NAME_HISTORY = 'ai-bookmark-history-update'
|
|
|
34
35
|
const { TextArea } = Input
|
|
35
36
|
const e = window.translate
|
|
36
37
|
|
|
37
|
-
function yieldToUI () {
|
|
38
|
-
return new Promise(resolve => {
|
|
39
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
40
|
-
requestAnimationFrame(() => resolve())
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
setTimeout(resolve, 0)
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
export default function AIBookmarkForm (props) {
|
|
48
39
|
const { onCancel } = props
|
|
49
40
|
const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
|
|
@@ -183,11 +174,23 @@ export default function AIBookmarkForm (props) {
|
|
|
183
174
|
setConfirmProgress({ current: 0, total: parsed.length })
|
|
184
175
|
|
|
185
176
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
177
|
+
const { error } = await runImportTask({
|
|
178
|
+
useModal: false,
|
|
179
|
+
batch: 50,
|
|
180
|
+
steps: [
|
|
181
|
+
{
|
|
182
|
+
items: parsed,
|
|
183
|
+
process: (chunk) => {
|
|
184
|
+
chunk.forEach(createBookmark)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
onProgress: (current, total) => {
|
|
189
|
+
setConfirmProgress({ current, total })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
if (error) {
|
|
193
|
+
throw error
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
setShowConfirm(false)
|
|
@@ -26,6 +26,7 @@ const bookmarkSchema = {
|
|
|
26
26
|
runScripts: 'array - run scripts after connected ({delay,script})',
|
|
27
27
|
quickCommands: 'array - quick commands ({name,command})',
|
|
28
28
|
proxy: 'string - proxy address (socks5://...)',
|
|
29
|
+
proxyCommand: 'string - ssh proxy command, supports %h (host) %p (port) %r (username) placeholders, e.g. `netbird ssh proxy %h %p` or `cloudflared access ssh --hostname %h`',
|
|
29
30
|
x11: 'boolean - enable x11 forwarding, default is false',
|
|
30
31
|
term: 'string - terminal type, default is xterm-256color, required',
|
|
31
32
|
displayRaw: 'boolean - display raw output, default is false',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// bookmark select tree (copied from legacy)
|
|
2
2
|
import createTitle from '../../../common/create-title'
|
|
3
|
+
import buildMap from '../../../common/build-map'
|
|
3
4
|
import { TreeSelect } from 'antd'
|
|
4
5
|
|
|
5
6
|
const e = window.translate
|
|
@@ -16,9 +17,9 @@ const hoppingProps = [
|
|
|
16
17
|
|
|
17
18
|
function buildTreeData (bookmarkGroups, tree) {
|
|
18
19
|
const cats = bookmarkGroups
|
|
19
|
-
const btree = cats
|
|
20
|
+
const btree = buildMap(cats)
|
|
20
21
|
function buildSubCats (id) {
|
|
21
|
-
const x = btree
|
|
22
|
+
const x = btree.get(id)
|
|
22
23
|
if (!x) return ''
|
|
23
24
|
const y = {
|
|
24
25
|
key: x.id,
|
|
@@ -34,7 +35,7 @@ function buildTreeData (bookmarkGroups, tree) {
|
|
|
34
35
|
return y
|
|
35
36
|
}
|
|
36
37
|
function buildLeaf (id) {
|
|
37
|
-
const x = tree
|
|
38
|
+
const x = tree.get(id)
|
|
38
39
|
if (!x) return ''
|
|
39
40
|
return { value: x.id, key: x.id, title: createTitle(x) }
|
|
40
41
|
}
|
|
@@ -58,10 +59,10 @@ function buildTreeData (bookmarkGroups, tree) {
|
|
|
58
59
|
|
|
59
60
|
export default function BookmarkSelect (props) {
|
|
60
61
|
const { bookmarks, bookmarkGroups } = props
|
|
61
|
-
const tree = bookmarks
|
|
62
|
+
const tree = buildMap(bookmarks)
|
|
62
63
|
|
|
63
64
|
function onSelect (id) {
|
|
64
|
-
const item = tree
|
|
65
|
+
const item = tree.get(id)
|
|
65
66
|
if (item) {
|
|
66
67
|
const selected = hoppingProps.reduce((p, k) => {
|
|
67
68
|
if (item[k] !== undefined) {
|
|
@@ -170,7 +170,7 @@ export function renderFormItem (item, formItemLayout, form, ctxProps, index) {
|
|
|
170
170
|
case 'execSettings':
|
|
171
171
|
return <ExecSettingsField key={name} />
|
|
172
172
|
case 'profileItem':
|
|
173
|
-
return <ProfileItem key={name} store={ctxProps.store} profileFilter={item.profileFilter} />
|
|
173
|
+
return <ProfileItem key={name} store={ctxProps.store} form={form} profileFilter={item.profileFilter} />
|
|
174
174
|
case 'quickCommands':
|
|
175
175
|
return <Fragment key={name}>{useQuickCmds(form, ctxProps.formData || {})}</Fragment>
|
|
176
176
|
case 'runScripts':
|
|
@@ -5,11 +5,20 @@
|
|
|
5
5
|
import React from 'react'
|
|
6
6
|
import { Form, Select } from 'antd'
|
|
7
7
|
import { formItemLayout } from '../../../common/form-layout'
|
|
8
|
+
import { authTypeMap } from '../../../common/constants'
|
|
8
9
|
|
|
9
10
|
const FormItem = Form.Item
|
|
10
11
|
const e = window.translate
|
|
11
12
|
|
|
12
|
-
export default function ProfileItem ({ store, profileFilter = (d) => d }) {
|
|
13
|
+
export default function ProfileItem ({ store, form, profileFilter = (d) => d }) {
|
|
14
|
+
const handleChange = (value) => {
|
|
15
|
+
// applyProfile only resolves credentials when authType is 'profiles',
|
|
16
|
+
// selecting a profile here means bookmark auth should come from it
|
|
17
|
+
form?.setFieldsValue({
|
|
18
|
+
authType: value ? authTypeMap.profiles : authTypeMap.password
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
const opts = {
|
|
14
23
|
options: store.profiles
|
|
15
24
|
.filter(profileFilter)
|
|
@@ -18,7 +27,8 @@ export default function ProfileItem ({ store, profileFilter = (d) => d }) {
|
|
|
18
27
|
value: d.id
|
|
19
28
|
})),
|
|
20
29
|
placeholder: e('profiles'),
|
|
21
|
-
allowClear: true
|
|
30
|
+
allowClear: true,
|
|
31
|
+
onChange: handleChange
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
return (
|
|
@@ -210,6 +210,16 @@ export const commonFields = {
|
|
|
210
210
|
label: () => e('proxy')
|
|
211
211
|
},
|
|
212
212
|
|
|
213
|
+
proxyCommand: {
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'proxyCommand',
|
|
216
|
+
label: () => `${e('proxy')} command`,
|
|
217
|
+
props: {
|
|
218
|
+
placeholder: 'netbird ssh proxy %h %p',
|
|
219
|
+
maxLength: 1024
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
213
223
|
x11: {
|
|
214
224
|
type: 'x11',
|
|
215
225
|
name: '__x11__',
|
|
@@ -281,6 +291,7 @@ export const sshSettings = [
|
|
|
281
291
|
},
|
|
282
292
|
commonFields.enableTerminalImage,
|
|
283
293
|
...terminalSettings.slice(0, -1), // All except terminalBackground
|
|
294
|
+
commonFields.proxyCommand,
|
|
284
295
|
commonFields.x11,
|
|
285
296
|
commonFields.terminalBackground
|
|
286
297
|
]
|
|
@@ -32,7 +32,7 @@ const rdpConfig = {
|
|
|
32
32
|
{ type: 'input', name: 'host', label: () => e('host'), rules: [{ required: true, message: e('host') + ' required' }] },
|
|
33
33
|
commonFields.port,
|
|
34
34
|
{ type: 'profileItem', name: '__profile__', label: '', profileFilter: d => !isEmpty(d.rdp) },
|
|
35
|
-
|
|
35
|
+
commonFields.username,
|
|
36
36
|
commonFields.password,
|
|
37
37
|
commonFields.description,
|
|
38
38
|
{ type: 'input', name: 'domain', label: () => e('domain') },
|
|
@@ -8,26 +8,15 @@ import { useState, useMemo } from 'react'
|
|
|
8
8
|
import { defaultBookmarkGroupId, settingMap } from '../../common/constants'
|
|
9
9
|
import deepCopy from 'json-deep-copy'
|
|
10
10
|
import createTitle, { createTitleWithTag } from '../../common/create-title'
|
|
11
|
+
import buildMap from '../../common/build-map'
|
|
11
12
|
|
|
12
13
|
const e = window.translate
|
|
13
14
|
|
|
14
15
|
function buildData (bookmarks, bookmarkGroups, searchText = '') {
|
|
15
16
|
const searchLower = searchText.toLowerCase()
|
|
16
17
|
const cats = bookmarkGroups
|
|
17
|
-
const tree = bookmarks
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
...p,
|
|
21
|
-
[k.id]: k
|
|
22
|
-
}
|
|
23
|
-
}, {})
|
|
24
|
-
const btree = cats
|
|
25
|
-
.reduce((p, k) => {
|
|
26
|
-
return {
|
|
27
|
-
...p,
|
|
28
|
-
[k.id]: k
|
|
29
|
-
}
|
|
30
|
-
}, {})
|
|
18
|
+
const tree = buildMap(bookmarks)
|
|
19
|
+
const btree = buildMap(cats)
|
|
31
20
|
|
|
32
21
|
// Helper to check if a node matches the search
|
|
33
22
|
function matchesSearch (text) {
|
|
@@ -37,7 +26,7 @@ function buildData (bookmarks, bookmarkGroups, searchText = '') {
|
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
function buildSubCats (id) {
|
|
40
|
-
const x = btree
|
|
29
|
+
const x = btree.get(id)
|
|
41
30
|
if (!x) {
|
|
42
31
|
return ''
|
|
43
32
|
}
|
|
@@ -64,7 +53,7 @@ function buildData (bookmarks, bookmarkGroups, searchText = '') {
|
|
|
64
53
|
return y
|
|
65
54
|
}
|
|
66
55
|
function buildLeaf (id) {
|
|
67
|
-
const x = tree
|
|
56
|
+
const x = tree.get(id)
|
|
68
57
|
if (!x) {
|
|
69
58
|
return ''
|
|
70
59
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared import progress modal.
|
|
3
|
+
*
|
|
4
|
+
* Mounted once in main.jsx (inside ConfigProvider, so antd theme applies)
|
|
5
|
+
* and driven imperatively through refsStatic by runImportTask
|
|
6
|
+
* (common/import-task.js) - same pattern as BatchOpRunner.
|
|
7
|
+
* Uses only existing utility classes (pd1y/mg1t/elli/common-err).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
11
|
+
import { Progress } from 'antd'
|
|
12
|
+
import Modal from './modal.jsx'
|
|
13
|
+
import { refsStatic } from './ref'
|
|
14
|
+
import { importProgressRefKey, cancelImportTask } from '../../common/import-task'
|
|
15
|
+
|
|
16
|
+
const e = window.translate
|
|
17
|
+
|
|
18
|
+
export default function ImportProgress () {
|
|
19
|
+
const [state, setState] = useState(null)
|
|
20
|
+
|
|
21
|
+
const show = useCallback((next) => {
|
|
22
|
+
setState(next)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const hide = useCallback(() => {
|
|
26
|
+
setState(null)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
refsStatic.add(importProgressRefKey, { show, hide })
|
|
31
|
+
return () => {
|
|
32
|
+
refsStatic.remove(importProgressRefKey)
|
|
33
|
+
}
|
|
34
|
+
}, [show, hide])
|
|
35
|
+
|
|
36
|
+
if (!state) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
const {
|
|
40
|
+
title,
|
|
41
|
+
status,
|
|
42
|
+
current,
|
|
43
|
+
total,
|
|
44
|
+
label,
|
|
45
|
+
error,
|
|
46
|
+
cancelable
|
|
47
|
+
} = state
|
|
48
|
+
const running = status === 'running'
|
|
49
|
+
const percent = Math.floor(current * 100 / (total || 1))
|
|
50
|
+
const modalProps = {
|
|
51
|
+
title: title || e('import'),
|
|
52
|
+
open: true,
|
|
53
|
+
width: 420,
|
|
54
|
+
zIndex: 5000,
|
|
55
|
+
maskClosable: false,
|
|
56
|
+
onCancel: running
|
|
57
|
+
? (cancelable ? cancelImportTask : undefined)
|
|
58
|
+
: hide,
|
|
59
|
+
footer: running && !cancelable
|
|
60
|
+
? null
|
|
61
|
+
: (
|
|
62
|
+
<div className='custom-modal-footer-buttons'>
|
|
63
|
+
<button
|
|
64
|
+
type='button'
|
|
65
|
+
className='custom-modal-cancel-btn'
|
|
66
|
+
onClick={running ? cancelImportTask : hide}
|
|
67
|
+
>
|
|
68
|
+
{e('cancel')}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
<Modal {...modalProps}>
|
|
75
|
+
<div className='pd1y'>
|
|
76
|
+
<Progress
|
|
77
|
+
percent={percent}
|
|
78
|
+
status={status === 'error' ? 'exception' : 'active'}
|
|
79
|
+
format={() => `${current}/${total}`}
|
|
80
|
+
/>
|
|
81
|
+
<div className='elli mg1t'>{label}</div>
|
|
82
|
+
{
|
|
83
|
+
error
|
|
84
|
+
? <div className='common-err mg1t'>{error.message || String(error)}</div>
|
|
85
|
+
: null
|
|
86
|
+
}
|
|
87
|
+
</div>
|
|
88
|
+
</Modal>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -151,7 +151,7 @@ export default auto(function FooterEntry (props) {
|
|
|
151
151
|
<div className='terminal-footer-unit terminal-footer-info'>
|
|
152
152
|
<InfoCircleOutlined
|
|
153
153
|
onClick={handleInfoPanel}
|
|
154
|
-
className='pointer
|
|
154
|
+
className='pointer font14 terminal-info-icon'
|
|
155
155
|
/>
|
|
156
156
|
</div>
|
|
157
157
|
)
|
|
@@ -38,6 +38,7 @@ import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-mod
|
|
|
38
38
|
import AutoSync from '../setting-sync/auto-sync'
|
|
39
39
|
import BatchOpRunner from '../batch-op/batch-op-runner'
|
|
40
40
|
import UnixTimestampTooltip from '../terminal/unix-timestamp-tooltip'
|
|
41
|
+
import ImportProgress from '../common/import-progress.jsx'
|
|
41
42
|
import { pick } from 'lodash-es'
|
|
42
43
|
import deepCopy from 'json-deep-copy'
|
|
43
44
|
import './wrapper.styl'
|
|
@@ -318,6 +319,7 @@ export default auto(function Index (props) {
|
|
|
318
319
|
<BookmarkFromHistoryModal />
|
|
319
320
|
<NotificationContainer />
|
|
320
321
|
<BatchOpRunner />
|
|
322
|
+
<ImportProgress />
|
|
321
323
|
<ShortcutBarEntry store={store} />
|
|
322
324
|
{!isAIDisabled() && <AIConfigModal store={store} />}
|
|
323
325
|
<UnixTimestampTooltip />
|