@electerm/electerm-react 5.1.25 → 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.
Files changed (32) hide show
  1. package/client/common/build-map.js +12 -0
  2. package/client/common/db.js +7 -5
  3. package/client/common/import-task.js +174 -0
  4. package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
  5. package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
  6. package/client/components/bookmark-form/common/fields.jsx +1 -1
  7. package/client/components/bookmark-form/common/profile-item.jsx +12 -2
  8. package/client/components/bookmark-form/config/rdp.js +1 -1
  9. package/client/components/bookmark-form/tree-select.jsx +5 -16
  10. package/client/components/common/import-progress.jsx +90 -0
  11. package/client/components/main/main.jsx +2 -0
  12. package/client/components/profile/profile-form-rdp.jsx +1 -1
  13. package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
  14. package/client/components/rdp/rdp-cred-form.jsx +55 -0
  15. package/client/components/rdp/rdp-session.jsx +97 -6
  16. package/client/components/setting-panel/keywords-transport.jsx +1 -4
  17. package/client/components/setting-panel/setting-modal.jsx +8 -2
  18. package/client/components/setting-panel/start-session-select.jsx +5 -16
  19. package/client/components/spice/spice-session.jsx +40 -0
  20. package/client/components/terminal/terminal-color-query.mjs +32 -27
  21. package/client/components/terminal/terminal.jsx +13 -9
  22. package/client/components/tree-list/bookmark-transport.jsx +2 -1
  23. package/client/components/tree-list/bookmark-upload.js +56 -41
  24. package/client/components/tree-list/tree-list-row.jsx +11 -4
  25. package/client/components/tree-list/tree-list-rows.js +6 -3
  26. package/client/components/tree-list/tree-list.jsx +4 -2
  27. package/client/store/common.js +19 -1
  28. package/client/store/load-data.js +6 -1
  29. package/client/store/store.js +3 -12
  30. package/client/store/sync.js +31 -2
  31. package/client/store/watch.js +10 -1
  32. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ /**
2
+ * build id -> item Map from array
3
+ * O(n), unlike reduce + object spread which is O(n²)
4
+ */
5
+
6
+ export default (arr = [], key = 'id') => {
7
+ const map = new Map()
8
+ for (const item of arr) {
9
+ map.set(item[key], item)
10
+ }
11
+ return map
12
+ }
@@ -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
- res.sort((a, b) => {
165
- const ai = order.findIndex(r => r === a.id)
166
- const bi = order.findIndex(r => r === b.id)
167
- return ai - bi
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
- for (let i = 0; i < parsed.length; i++) {
187
- // Yield between synchronous store mutations so large imports stay responsive.
188
- await yieldToUI()
189
- createBookmark(parsed[i])
190
- setConfirmProgress({ current: i + 1, total: parsed.length })
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.reduce((p, k) => ({ ...p, [k.id]: k }), {})
20
+ const btree = buildMap(cats)
20
21
  function buildSubCats (id) {
21
- const x = btree[id]
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[id]
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.reduce((p, k) => ({ ...p, [k.id]: k }), {})
62
+ const tree = buildMap(bookmarks)
62
63
 
63
64
  function onSelect (id) {
64
- const item = tree[id]
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
- { ...commonFields.username, rules: [{ required: true, message: e('username') + ' required' }] },
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
- .reduce((p, k) => {
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[id]
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[id]
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
+ }
@@ -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 />
@@ -15,7 +15,7 @@ export default function ProfileFormRdp (props) {
15
15
  {...formItemLayout}
16
16
  label={e('username')}
17
17
  hasFeedback
18
- name={['rdp', 'userName']}
18
+ name={['rdp', 'username']}
19
19
  >
20
20
  <Input />
21
21
  </FormItem>
@@ -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 copy from 'json-deep-copy'
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 arrOld = copy(state)
17
- const bmTreeOld = arrOld.reduce((p, v) => {
18
- return {
19
- ...p,
20
- [v.id]: v
21
- }
22
- }, {})
23
- arr.forEach(bg => {
24
- if (!bmTreeOld[bg.id]) {
25
- state.push(bg)
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 (e) {
29
- store.onError(e)
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
+ }