@electerm/electerm-react 5.1.23 → 5.2.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/db.js +7 -5
- package/client/common/import-task.js +174 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
- 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/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/layout/layout.jsx +6 -4
- 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/spice/spice-session.jsx +40 -0
- package/client/components/terminal/shortcut-bar-entry.jsx +4 -2
- package/client/components/terminal/shortcut-bar.jsx +59 -2
- package/client/components/terminal/shortcut-bar.styl +4 -2
- package/client/components/terminal/terminal-color-query.mjs +32 -27
- package/client/components/terminal/terminal.jsx +13 -9
- 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 +19 -1
- package/client/store/init-state.js +3 -0
- package/client/store/load-data.js +6 -1
- package/client/store/store.js +3 -12
- package/client/store/sync.js +31 -2
- package/client/store/watch.js +10 -1
- package/package.json +1 -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
|
+
}
|
|
@@ -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)
|
|
@@ -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 (
|
|
@@ -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
|
+
}
|
|
@@ -43,9 +43,10 @@ export default auto(function Layout (props) {
|
|
|
43
43
|
rightPanelWidth,
|
|
44
44
|
resizeTrigger,
|
|
45
45
|
inActiveTerminal,
|
|
46
|
-
shortcutBarVisible
|
|
46
|
+
shortcutBarVisible,
|
|
47
|
+
shortcutBarKbOffset
|
|
47
48
|
} = props.store
|
|
48
|
-
const h = height - footerHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight : 0) + resizeTrigger
|
|
49
|
+
const h = height - footerHeight - (inActiveTerminal && pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
|
|
49
50
|
const l = pinned ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
|
|
50
51
|
const r = rightPanelVisible && rightPanelPinned ? rightPanelWidth : 0
|
|
51
52
|
return {
|
|
@@ -68,14 +69,15 @@ export default auto(function Layout (props) {
|
|
|
68
69
|
rightPanelPinned,
|
|
69
70
|
rightPanelWidth,
|
|
70
71
|
pinned,
|
|
71
|
-
shortcutBarVisible
|
|
72
|
+
shortcutBarVisible,
|
|
73
|
+
shortcutBarKbOffset
|
|
72
74
|
} = props.store
|
|
73
75
|
const l = pinned ? leftSidePanelWidth : 0
|
|
74
76
|
const r = rightPanelPinned && rightPanelVisible ? rightPanelWidth : 0
|
|
75
77
|
// account for the far-left icon bar (sidebarWidth - 1px border on desktop;
|
|
76
78
|
// 0 when the bar is hidden on mobile)
|
|
77
79
|
const w = width - l - r - (leftSideBarWidth > 0 ? leftSideBarWidth - 1 : 0)
|
|
78
|
-
const h = height - footerHeight - (pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight : 0)
|
|
80
|
+
const h = height - footerHeight - (pinnedQuickCommandBar ? quickCommandBoxHeight : 0) - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0)
|
|
79
81
|
return layoutAlg(layout, w, h)
|
|
80
82
|
}
|
|
81
83
|
const layoutSize = calcLayoutStyle()
|
|
@@ -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 />
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import BookmarkTransport from '../tree-list/bookmark-transport'
|
|
2
2
|
import download from '../../common/download'
|
|
3
3
|
import time from '../../common/time'
|
|
4
|
-
import
|
|
4
|
+
import { runImportTask } from '../../common/import-task'
|
|
5
|
+
|
|
6
|
+
const e = window.translate
|
|
5
7
|
|
|
6
8
|
export default class QmTransport extends BookmarkTransport {
|
|
7
9
|
name = 'quickCommands'
|
|
10
|
+
|
|
8
11
|
beforeUpload = async (file) => {
|
|
9
12
|
const { store } = this.props
|
|
10
13
|
const txt = file.fileContent !== undefined
|
|
@@ -13,28 +16,31 @@ export default class QmTransport extends BookmarkTransport {
|
|
|
13
16
|
try {
|
|
14
17
|
const arr = JSON.parse(txt)
|
|
15
18
|
const state = store[this.name]
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const existing = new Set(state.map(v => v.id))
|
|
20
|
+
const fresh = arr.filter(bg => !existing.has(bg.id))
|
|
21
|
+
if (!fresh.length) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
await runImportTask({
|
|
25
|
+
title: e('import'),
|
|
26
|
+
batch: 200,
|
|
27
|
+
stopWatchers: [this.name],
|
|
28
|
+
steps: [
|
|
29
|
+
{
|
|
30
|
+
label: e('quickCommands'),
|
|
31
|
+
items: fresh,
|
|
32
|
+
process: (chunk) => {
|
|
33
|
+
state.push(...chunk)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
27
37
|
})
|
|
28
|
-
} catch (
|
|
29
|
-
store.onError(
|
|
38
|
+
} catch (err) {
|
|
39
|
+
store.onError(err)
|
|
30
40
|
}
|
|
31
41
|
return false
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
renderEdit () {
|
|
35
|
-
return null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
44
|
handleDownload = () => {
|
|
39
45
|
const { store } = this.props
|
|
40
46
|
const arr = store[this.name]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RDP credentials prompt form, shown at connect time
|
|
3
|
+
* when username/password are missing from the bookmark
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Input,
|
|
8
|
+
Form,
|
|
9
|
+
Button
|
|
10
|
+
} from 'antd'
|
|
11
|
+
import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
|
|
12
|
+
import Password from '../common/password'
|
|
13
|
+
|
|
14
|
+
const FormItem = Form.Item
|
|
15
|
+
const e = window.translate
|
|
16
|
+
|
|
17
|
+
export default function RdpCredForm (props) {
|
|
18
|
+
const [form] = Form.useForm()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Form
|
|
22
|
+
form={form}
|
|
23
|
+
onFinish={props.handleFinish}
|
|
24
|
+
initialValues={props.initialValues || {}}
|
|
25
|
+
name='rdp-cred-form'
|
|
26
|
+
>
|
|
27
|
+
<div className='pd3t pd1b'>
|
|
28
|
+
<FormItem
|
|
29
|
+
{...formItemLayout}
|
|
30
|
+
label={e('username')}
|
|
31
|
+
name='username'
|
|
32
|
+
>
|
|
33
|
+
<Input autoFocus />
|
|
34
|
+
</FormItem>
|
|
35
|
+
<FormItem
|
|
36
|
+
{...formItemLayout}
|
|
37
|
+
label={e('password')}
|
|
38
|
+
name='password'
|
|
39
|
+
>
|
|
40
|
+
<Password />
|
|
41
|
+
</FormItem>
|
|
42
|
+
<FormItem
|
|
43
|
+
{...tailFormItemLayout}
|
|
44
|
+
>
|
|
45
|
+
<Button
|
|
46
|
+
type='primary'
|
|
47
|
+
htmlType='submit'
|
|
48
|
+
>
|
|
49
|
+
{e('connect')}
|
|
50
|
+
</Button>
|
|
51
|
+
</FormItem>
|
|
52
|
+
</div>
|
|
53
|
+
</Form>
|
|
54
|
+
)
|
|
55
|
+
}
|