@electerm/electerm-react 3.8.8 → 3.9.5
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/parse-quick-connect.js +9 -1
- package/client/common/pre.js +0 -1
- package/client/components/ai/ai-config-modal.jsx +52 -0
- package/client/components/ai/ai-config.jsx +5 -38
- package/client/components/ai/get-brand.js +0 -11
- package/client/components/bg/custom-css.jsx +2 -1
- package/client/components/bookmark-form/bookmark-from-history-modal.jsx +0 -1
- package/client/components/bookmark-form/config/rdp.js +4 -2
- package/client/components/icons/heartbeat.jsx +23 -0
- package/client/components/main/main.jsx +5 -1
- package/client/components/session/session.jsx +3 -3
- package/client/components/setting-sync/setting-sync-form.jsx +9 -1
- package/client/components/setting-sync/setting-sync.jsx +2 -1
- package/client/components/sftp/list-table-ui.jsx +29 -54
- package/client/components/sftp/paged-list.jsx +44 -44
- package/client/components/sftp/sftp-entry.jsx +5 -4
- package/client/components/sidebar/info-modal.jsx +8 -2
- package/client/components/terminal/reconnect-overlay.jsx +2 -15
- package/client/components/terminal/terminal-error-handle.jsx +43 -0
- package/client/components/terminal/terminal.jsx +38 -38
- package/client/components/terminal/terminal.styl +12 -7
- package/client/components/terminal/unix-timestamp-tooltip.jsx +85 -0
- package/client/components/tree-list/bookmark-toolbar.jsx +9 -71
- package/client/components/tree-list/bookmark-upload.js +106 -0
- package/client/components/widgets/widget-control.jsx +1 -0
- package/client/store/common.js +3 -11
- package/client/store/init-state.js +1 -0
- package/client/store/load-data.js +2 -2
- package/client/store/sync.js +3 -2
- package/client/store/tab.js +20 -0
- package/package.json +1 -1
- package/client/components/ai/providers.js +0 -14
- package/client/components/terminal/socket-close-warning.jsx +0 -94
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
|
|
22
22
|
const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm']
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Deny list for opts keys - these are parsed from the URL itself
|
|
26
|
+
* and should not be overridable via the opts JSON parameter for safety
|
|
27
|
+
*/
|
|
28
|
+
const OPTS_DENY_LIST = ['type', 'host']
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Default ports for each protocol
|
|
26
32
|
*/
|
|
@@ -393,6 +399,7 @@ function parseQuickConnect (str) {
|
|
|
393
399
|
if (optsStr) {
|
|
394
400
|
try {
|
|
395
401
|
const extraOpts = JSON.parse(optsStr)
|
|
402
|
+
OPTS_DENY_LIST.forEach(key => delete extraOpts[key])
|
|
396
403
|
Object.assign(opts, extraOpts)
|
|
397
404
|
} catch (err) {
|
|
398
405
|
console.error('Failed to parse opts:', err)
|
|
@@ -439,5 +446,6 @@ export {
|
|
|
439
446
|
getDefaultPort,
|
|
440
447
|
getSupportedProtocols,
|
|
441
448
|
SUPPORTED_PROTOCOLS,
|
|
442
|
-
DEFAULT_PORTS
|
|
449
|
+
DEFAULT_PORTS,
|
|
450
|
+
OPTS_DENY_LIST
|
|
443
451
|
}
|
package/client/common/pre.js
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Modal from '../common/modal'
|
|
2
|
+
import { auto } from 'manate/react'
|
|
3
|
+
import AIConfigForm from './ai-config'
|
|
4
|
+
import message from '../common/message'
|
|
5
|
+
import { aiConfigsArr } from './ai-config-props'
|
|
6
|
+
import { pick } from 'lodash-es'
|
|
7
|
+
|
|
8
|
+
const e = window.translate
|
|
9
|
+
|
|
10
|
+
export default auto(function AIConfigModal ({ store }) {
|
|
11
|
+
const { showAIConfigModal } = store
|
|
12
|
+
|
|
13
|
+
if (!showAIConfigModal) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getInitialValues () {
|
|
18
|
+
const res = pick(store.config, aiConfigsArr)
|
|
19
|
+
if (!res.languageAI) {
|
|
20
|
+
res.languageAI = window.store.getLangName()
|
|
21
|
+
}
|
|
22
|
+
return res
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleSubmit (values) {
|
|
26
|
+
window.store.updateConfig(values)
|
|
27
|
+
message.success(e('saved') || 'Saved')
|
|
28
|
+
window.store.showAIConfigModal = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleClose () {
|
|
32
|
+
window.store.showAIConfigModal = false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Modal
|
|
37
|
+
open={showAIConfigModal}
|
|
38
|
+
onCancel={handleClose}
|
|
39
|
+
footer={null}
|
|
40
|
+
title='AI Config'
|
|
41
|
+
width='80%'
|
|
42
|
+
destroyOnClose
|
|
43
|
+
className='ai-config-modal'
|
|
44
|
+
>
|
|
45
|
+
<AIConfigForm
|
|
46
|
+
initialValues={getInitialValues()}
|
|
47
|
+
onSubmit={handleSubmit}
|
|
48
|
+
showAIConfig
|
|
49
|
+
/>
|
|
50
|
+
</Modal>
|
|
51
|
+
)
|
|
52
|
+
})
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Alert,
|
|
7
7
|
Space
|
|
8
8
|
} from 'antd'
|
|
9
|
-
import { useEffect
|
|
9
|
+
import { useEffect } from 'react'
|
|
10
10
|
import Link from '../common/external-link'
|
|
11
11
|
import AiCache from './ai-cache'
|
|
12
12
|
import {
|
|
@@ -15,9 +15,6 @@ import {
|
|
|
15
15
|
import Password from '../common/password'
|
|
16
16
|
import AiHistory, { addHistoryItem } from './ai-history'
|
|
17
17
|
|
|
18
|
-
// Comprehensive API provider configurations
|
|
19
|
-
import providers from './providers'
|
|
20
|
-
|
|
21
18
|
const STORAGE_KEY_CONFIG = 'ai_config_history'
|
|
22
19
|
const EVENT_NAME_CONFIG = 'ai-config-history-update'
|
|
23
20
|
|
|
@@ -39,7 +36,6 @@ const proxyOptions = [
|
|
|
39
36
|
|
|
40
37
|
export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
|
|
41
38
|
const [form] = Form.useForm()
|
|
42
|
-
const [modelOptions, setModelOptions] = useState([])
|
|
43
39
|
|
|
44
40
|
useEffect(() => {
|
|
45
41
|
if (initialValues) {
|
|
@@ -51,23 +47,6 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
51
47
|
return true
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
const getBaseURLOptions = () => {
|
|
55
|
-
return providers.map(provider => ({
|
|
56
|
-
value: provider.baseURL,
|
|
57
|
-
label: provider.label
|
|
58
|
-
}))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const getModelOptions = (baseURL) => {
|
|
62
|
-
const provider = providers.find(p => p.baseURL === baseURL)
|
|
63
|
-
if (!provider) return []
|
|
64
|
-
|
|
65
|
-
return provider.models.map(model => ({
|
|
66
|
-
value: model,
|
|
67
|
-
label: model
|
|
68
|
-
}))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
50
|
const handleSubmit = async (values) => {
|
|
72
51
|
onSubmit(values)
|
|
73
52
|
addHistoryItem(STORAGE_KEY_CONFIG, values, EVENT_NAME_CONFIG)
|
|
@@ -88,11 +67,6 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
88
67
|
return { label, title }
|
|
89
68
|
}
|
|
90
69
|
|
|
91
|
-
function handleChange (v) {
|
|
92
|
-
const options = getModelOptions(v)
|
|
93
|
-
setModelOptions(options)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
70
|
if (!showAIConfig) {
|
|
97
71
|
return null
|
|
98
72
|
}
|
|
@@ -127,12 +101,8 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
127
101
|
{ type: 'url', message: 'Please enter a valid URL!' }
|
|
128
102
|
]}
|
|
129
103
|
>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
placeholder='Enter or select API provider URL'
|
|
133
|
-
filterOption={filter}
|
|
134
|
-
onChange={handleChange}
|
|
135
|
-
allowClear
|
|
104
|
+
<Input
|
|
105
|
+
placeholder='Enter API provider URL'
|
|
136
106
|
style={{ width: '75%' }}
|
|
137
107
|
/>
|
|
138
108
|
</Form.Item>
|
|
@@ -156,10 +126,8 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
156
126
|
name='modelAI'
|
|
157
127
|
rules={[{ required: true, message: 'Please input or select a model!' }]}
|
|
158
128
|
>
|
|
159
|
-
<
|
|
160
|
-
options={modelOptions}
|
|
129
|
+
<Input
|
|
161
130
|
placeholder='Enter or select AI model'
|
|
162
|
-
filterOption={filter}
|
|
163
131
|
/>
|
|
164
132
|
</Form.Item>
|
|
165
133
|
|
|
@@ -202,11 +170,10 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
202
170
|
>
|
|
203
171
|
<AutoComplete
|
|
204
172
|
options={proxyOptions}
|
|
205
|
-
placeholder='Enter proxy URL (optional)'
|
|
206
173
|
filterOption={filter}
|
|
207
174
|
allowClear
|
|
208
175
|
>
|
|
209
|
-
<Input />
|
|
176
|
+
<Input placeholder='Enter proxy URL (optional)' />
|
|
210
177
|
</AutoComplete>
|
|
211
178
|
</Form.Item>
|
|
212
179
|
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import providers from './providers'
|
|
2
|
-
|
|
3
1
|
export default function getBrand (baseURLAI) {
|
|
4
|
-
// First, try to match with providers
|
|
5
|
-
const provider = providers.find(p => p.baseURL === baseURLAI)
|
|
6
|
-
if (provider) {
|
|
7
|
-
return {
|
|
8
|
-
brand: provider.label,
|
|
9
|
-
brandUrl: provider.homepage
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
2
|
// If no match, extract brand from URL
|
|
14
3
|
try {
|
|
15
4
|
const url = new URL(baseURLAI)
|
|
@@ -13,7 +13,8 @@ export default function CustomCss (props) {
|
|
|
13
13
|
if (configLoaded) {
|
|
14
14
|
const style = document.getElementById(themeDomId)
|
|
15
15
|
if (style) {
|
|
16
|
-
|
|
16
|
+
const safeCss = (customCss || '').replace(/@import/gi, '#')
|
|
17
|
+
style.innerHTML = safeCss
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}, [customCss, configLoaded])
|
|
@@ -2,7 +2,7 @@ import { formItemLayout } from '../../../common/form-layout.js'
|
|
|
2
2
|
import { terminalRdpType } from '../../../common/constants.js'
|
|
3
3
|
import { createBaseInitValues, getAuthTypeDefault } from '../common/init-values.js'
|
|
4
4
|
import { isEmpty } from 'lodash-es'
|
|
5
|
-
import { commonFields } from './common-fields.js'
|
|
5
|
+
import { commonFields, connectionHoppingTab } from './common-fields.js'
|
|
6
6
|
|
|
7
7
|
const e = window.translate
|
|
8
8
|
|
|
@@ -12,6 +12,7 @@ const rdpConfig = {
|
|
|
12
12
|
initValues: (props) => {
|
|
13
13
|
return createBaseInitValues(props, terminalRdpType, {
|
|
14
14
|
port: 3389,
|
|
15
|
+
connectionHoppings: [],
|
|
15
16
|
...getAuthTypeDefault(props)
|
|
16
17
|
})
|
|
17
18
|
},
|
|
@@ -38,7 +39,8 @@ const rdpConfig = {
|
|
|
38
39
|
commonFields.proxy,
|
|
39
40
|
commonFields.type
|
|
40
41
|
]
|
|
41
|
-
}
|
|
42
|
+
},
|
|
43
|
+
connectionHoppingTab()
|
|
42
44
|
]
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Icon from '@ant-design/icons'
|
|
2
|
+
|
|
3
|
+
const HeartbeatSvg = () => (
|
|
4
|
+
<svg
|
|
5
|
+
viewBox='0 0 1024 1024'
|
|
6
|
+
fill='currentColor'
|
|
7
|
+
width='1em'
|
|
8
|
+
height='1em'
|
|
9
|
+
aria-hidden='true'
|
|
10
|
+
>
|
|
11
|
+
<path d='M923 283.6a260.1 260.1 0 00-56.9-82.8 264.4 264.4 0 00-84-55.5A265.3 265.3 0 00679.7 128c-38.1 0-75.4 7.5-109.8 22.3a274.5 274.5 0 00-87.7 60.8l-12.1 12.5-12.1-12.5a260.5 260.5 0 00-87.7-60.8c-34.4-14.8-71.7-22.3-109.8-22.3-38.2 0-75.5 7.5-109.9 22.3-33.4 14.3-63.3 34.9-88.9 61-25.6 26.1-45.7 56.4-59.9 90.5A278.3 278.3 0 000 416.5c0 39.6 7.7 77.2 22.9 111.6 12.8 29.6 31.5 57 55.5 81.5l356.2 358.5c12.2 12.3 29.7 19.4 47.8 19.4 18.1 0 35.6-7.1 47.7-19.4L886 609.6c24-24.5 42.7-51.9 55.5-81.5C956.3 493.7 964 456.1 964 416.5c0-37.9-7.4-74.7-22-109zM880 497.9c-10.4 24.1-26 46-46.5 65.2L480 920.7 193.5 563.1C173 543.9 157.4 522 147 497.9c-12.1-27.9-18.2-57.8-18.2-88.6 0-30 5.8-59 17.3-86.3 11.1-26.5 27.2-50.3 47.8-70.8 20.6-20.4 44.6-36.5 71.4-47.8 27.7-11.7 57.2-17.7 87.8-17.7 32.3 0 63.7 6.5 93.2 19.3 29 12.5 55 30.9 77.2 54.4l73.9 76.5 73.9-76.5c22.2-23.5 48.2-41.9 77.2-54.4 29.5-12.8 60.9-19.3 93.2-19.3 30.6 0 60.1 6 87.8 17.7 26.8 11.3 50.8 27.4 71.4 47.8 20.6 20.5 36.7 44.3 47.8 70.8 11.5 27.3 17.3 56.3 17.3 86.3 0 30.8-6.1 60.7-18.2 88.6z' />
|
|
12
|
+
<polyline
|
|
13
|
+
points='160,512 310,512 370,310 450,714 530,512 594,512 654,360 714,664 774,512 864,512'
|
|
14
|
+
fill='none'
|
|
15
|
+
stroke='currentColor'
|
|
16
|
+
strokeWidth='60'
|
|
17
|
+
strokeLinecap='round'
|
|
18
|
+
strokeLinejoin='round'
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const HeartbeatIcon = props => (<Icon component={HeartbeatSvg} {...props} />)
|
|
@@ -28,6 +28,7 @@ import ConnectionHoppingWarning from './connection-hopping-warnning'
|
|
|
28
28
|
import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
|
|
29
29
|
import LoadSshConfigs from '../ssh-config/load-ssh-configs'
|
|
30
30
|
import AIChat from '../ai/ai-chat'
|
|
31
|
+
import AIConfigModal from '../ai/ai-config-modal'
|
|
31
32
|
import Opacity from '../common/opacity'
|
|
32
33
|
import MoveItemModal from '../tree-list/move-item-modal'
|
|
33
34
|
import InputContextMenu from '../common/input-context-menu'
|
|
@@ -35,6 +36,7 @@ import WorkspaceSaveModal from '../tabs/workspace-save-modal'
|
|
|
35
36
|
import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
|
|
36
37
|
import AutoSync from '../setting-sync/auto-sync'
|
|
37
38
|
import BatchOpRunner from '../batch-op/batch-op-runner'
|
|
39
|
+
import UnixTimestampTooltip from '../terminal/unix-timestamp-tooltip'
|
|
38
40
|
import { pick } from 'lodash-es'
|
|
39
41
|
import deepCopy from 'json-deep-copy'
|
|
40
42
|
import './wrapper.styl'
|
|
@@ -50,7 +52,7 @@ export default auto(function Index (props) {
|
|
|
50
52
|
ipcOnEvent('open-about', store.openAbout)
|
|
51
53
|
ipcOnEvent('new-ssh', store.onNewSsh)
|
|
52
54
|
ipcOnEvent('add-tab-from-command-line', store.addTabFromCommandLine)
|
|
53
|
-
ipcOnEvent('open-tab', (e, parsed) => store.
|
|
55
|
+
ipcOnEvent('open-tab', (e, parsed) => store.ipcOpenTab(parsed))
|
|
54
56
|
ipcOnEvent('openSettings', store.openSetting)
|
|
55
57
|
ipcOnEvent('selectall', store.selectall)
|
|
56
58
|
ipcOnEvent('focused', store.focus)
|
|
@@ -295,6 +297,8 @@ export default auto(function Index (props) {
|
|
|
295
297
|
<BookmarkFromHistoryModal />
|
|
296
298
|
<NotificationContainer />
|
|
297
299
|
<BatchOpRunner />
|
|
300
|
+
<AIConfigModal store={store} />
|
|
301
|
+
<UnixTimestampTooltip />
|
|
298
302
|
</div>
|
|
299
303
|
</ConfigProvider>
|
|
300
304
|
)
|
|
@@ -14,8 +14,7 @@ import {
|
|
|
14
14
|
FullscreenOutlined,
|
|
15
15
|
PaperClipOutlined,
|
|
16
16
|
CloseOutlined,
|
|
17
|
-
ApartmentOutlined
|
|
18
|
-
HeartOutlined
|
|
17
|
+
ApartmentOutlined
|
|
19
18
|
} from '@ant-design/icons'
|
|
20
19
|
import {
|
|
21
20
|
Tooltip,
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
import { SplitViewIcon } from '../icons/split-view'
|
|
38
37
|
import { refs } from '../common/ref'
|
|
39
38
|
import safeName from '../../common/safe-name'
|
|
39
|
+
import { HeartbeatIcon } from '../icons/heartbeat'
|
|
40
40
|
import './session.styl'
|
|
41
41
|
|
|
42
42
|
const e = window.translate
|
|
@@ -550,7 +550,7 @@ export default class SessionWrapper extends Component {
|
|
|
550
550
|
}
|
|
551
551
|
return (
|
|
552
552
|
<Tooltip title={title}>
|
|
553
|
-
<
|
|
553
|
+
<HeartbeatIcon {...iconProps} />
|
|
554
554
|
</Tooltip>
|
|
555
555
|
)
|
|
556
556
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useEffect, useRef } from 'react'
|
|
9
9
|
import { ArrowDownOutlined, ArrowUpOutlined, SaveOutlined, ClearOutlined } from '@ant-design/icons'
|
|
10
|
-
import { Button, Input, Form, Alert } from 'antd'
|
|
10
|
+
import { Button, Input, Form, Alert, Switch } from 'antd'
|
|
11
11
|
import { notification } from '../common/notification'
|
|
12
12
|
import Link from '../common/external-link'
|
|
13
13
|
import dayjs from 'dayjs'
|
|
@@ -80,6 +80,7 @@ export default function SyncForm (props) {
|
|
|
80
80
|
up[syncType + 'ServerUrl'] = res.serverUrl || ''
|
|
81
81
|
up[syncType + 'Username'] = res.username || ''
|
|
82
82
|
up[syncType + 'Password'] = res.password || ''
|
|
83
|
+
up[syncType + 'SkipVerify'] = res.skipVerify || false
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
window.store.updateSyncSetting(up)
|
|
@@ -247,6 +248,13 @@ export default function SyncForm (props) {
|
|
|
247
248
|
id='sync-input-webdav-username'
|
|
248
249
|
/>
|
|
249
250
|
</FormItem>
|
|
251
|
+
<FormItem
|
|
252
|
+
label={createLabel('Skip SSL verify')}
|
|
253
|
+
name='skipVerify'
|
|
254
|
+
valuePropName='checked'
|
|
255
|
+
>
|
|
256
|
+
<Switch />
|
|
257
|
+
</FormItem>
|
|
250
258
|
<FormItem
|
|
251
259
|
label={createLabel(e('password'))}
|
|
252
260
|
name='password'
|
|
@@ -48,7 +48,8 @@ export default auto(function SyncSettingEntry (props) {
|
|
|
48
48
|
// WebDAV specific fields
|
|
49
49
|
serverUrl: syncSetting[type + 'ServerUrl'],
|
|
50
50
|
username: syncSetting[type + 'Username'],
|
|
51
|
-
password: syncSetting[type + 'Password']
|
|
51
|
+
password: syncSetting[type + 'Password'],
|
|
52
|
+
skipVerify: syncSetting[type + 'SkipVerify'] || false
|
|
52
53
|
}
|
|
53
54
|
return (
|
|
54
55
|
<SyncForm
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* file list table
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Component } from 'react'
|
|
6
|
-
import { Dropdown
|
|
5
|
+
import { Component, createRef } from 'react'
|
|
6
|
+
import { Dropdown } from 'antd'
|
|
7
7
|
import classnames from 'classnames'
|
|
8
8
|
import FileSection from './file-item'
|
|
9
9
|
import PagedList from './paged-list'
|
|
@@ -19,7 +19,25 @@ const e = window.translate
|
|
|
19
19
|
export default class FileListTable extends Component {
|
|
20
20
|
constructor (props) {
|
|
21
21
|
super(props)
|
|
22
|
-
this.state =
|
|
22
|
+
this.state = {
|
|
23
|
+
...this.initFromProps(),
|
|
24
|
+
scrollTop: 0
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
containerRef = createRef()
|
|
29
|
+
|
|
30
|
+
componentDidUpdate (prevProps) {
|
|
31
|
+
if (prevProps.fileList !== this.props.fileList) {
|
|
32
|
+
if (this.containerRef.current) {
|
|
33
|
+
this.containerRef.current.scrollTop = 0
|
|
34
|
+
}
|
|
35
|
+
this.setState({ scrollTop: 0 })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onScroll = (e) => {
|
|
40
|
+
this.setState({ scrollTop: e.target.scrollTop })
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
initFromProps = (pps = this.getPropsDefault()) => {
|
|
@@ -36,9 +54,7 @@ export default class FileListTable extends Component {
|
|
|
36
54
|
}
|
|
37
55
|
})
|
|
38
56
|
return {
|
|
39
|
-
|
|
40
|
-
properties,
|
|
41
|
-
page: 1
|
|
57
|
+
properties
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
|
|
@@ -171,40 +187,6 @@ export default class FileListTable extends Component {
|
|
|
171
187
|
'left'
|
|
172
188
|
]
|
|
173
189
|
|
|
174
|
-
hasPager = () => {
|
|
175
|
-
const {
|
|
176
|
-
pageSize
|
|
177
|
-
} = this.state
|
|
178
|
-
const {
|
|
179
|
-
fileList
|
|
180
|
-
} = this.props
|
|
181
|
-
const len = fileList.length
|
|
182
|
-
return len > pageSize
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
onPageChange = (page) => {
|
|
186
|
-
this.setState({ page })
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
renderPager () {
|
|
190
|
-
const { page, pageSize } = this.state
|
|
191
|
-
const { fileList } = this.props
|
|
192
|
-
const props = {
|
|
193
|
-
current: page,
|
|
194
|
-
pageSize,
|
|
195
|
-
total: fileList.length,
|
|
196
|
-
showLessItems: true,
|
|
197
|
-
showSizeChanger: false,
|
|
198
|
-
simple: false,
|
|
199
|
-
onChange: this.onPageChange
|
|
200
|
-
}
|
|
201
|
-
return (
|
|
202
|
-
<div className='pd1b pager-wrap'>
|
|
203
|
-
<Pagination {...props} />
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
190
|
// reset
|
|
209
191
|
resetWidth = () => {
|
|
210
192
|
this.setState(this.initFromProps())
|
|
@@ -293,24 +275,19 @@ export default class FileListTable extends Component {
|
|
|
293
275
|
|
|
294
276
|
render () {
|
|
295
277
|
const { fileList, height, type } = this.props
|
|
296
|
-
|
|
297
|
-
// const sh = sshSftpSplitView ? 0 : 32
|
|
278
|
+
const containerHeight = height - 42 - 30 - 32 - 90
|
|
298
279
|
const props = {
|
|
280
|
+
ref: this.containerRef,
|
|
299
281
|
className: 'sftp-table-content overscroll-y relative',
|
|
300
282
|
style: {
|
|
301
|
-
height:
|
|
283
|
+
height: containerHeight
|
|
302
284
|
},
|
|
303
285
|
draggable: false,
|
|
286
|
+
onScroll: this.onScroll,
|
|
304
287
|
onClick: this.handleClick,
|
|
305
288
|
onDoubleClick: this.handleDoubleClick
|
|
306
289
|
}
|
|
307
|
-
const
|
|
308
|
-
const cls = classnames(
|
|
309
|
-
'sftp-table relative',
|
|
310
|
-
{
|
|
311
|
-
'sftp-has-pager': hasPager
|
|
312
|
-
}
|
|
313
|
-
)
|
|
290
|
+
const cls = classnames('sftp-table relative')
|
|
314
291
|
const ddProps = {
|
|
315
292
|
menu: {
|
|
316
293
|
items: this.renderContextMenuFile(),
|
|
@@ -338,13 +315,11 @@ export default class FileListTable extends Component {
|
|
|
338
315
|
<PagedList
|
|
339
316
|
list={fileList}
|
|
340
317
|
renderItem={this.renderItem}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
pageSize={this.state.pageSize}
|
|
318
|
+
scrollTop={this.state.scrollTop}
|
|
319
|
+
containerHeight={containerHeight}
|
|
344
320
|
/>
|
|
345
321
|
</div>
|
|
346
322
|
</Dropdown>
|
|
347
|
-
{hasPager && this.renderPager()}
|
|
348
323
|
</div>
|
|
349
324
|
)
|
|
350
325
|
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Virtual list for SFTP file list.
|
|
3
|
+
*
|
|
4
|
+
* Scroll state is owned by the parent (list-table-ui) via React's onScroll prop
|
|
5
|
+
* on the scrollable container, and passed down as `scrollTop`. This avoids the
|
|
6
|
+
* React lifecycle ordering bug where a child's componentDidMount fires before the
|
|
7
|
+
* parent div's ref is assigned, making addEventListener attach to null.
|
|
8
|
+
*
|
|
9
|
+
* Uses spacers (top/bottom divs) so the container's scrollbar reflects the full
|
|
10
|
+
* list height while only the visible window (± OVERSCAN) is in the DOM.
|
|
11
|
+
*
|
|
12
|
+
* offsetTop is read from rootRef.offsetTop at render time — the distance from the
|
|
13
|
+
* scroll container's top edge to this list's top edge (accounts for the ".." parent
|
|
14
|
+
* row above the list).
|
|
3
15
|
*/
|
|
4
16
|
|
|
5
|
-
import { Component } from 'react'
|
|
6
|
-
import { Pagination } from 'antd'
|
|
17
|
+
import { Component, createRef } from 'react'
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
page: 1,
|
|
11
|
-
pageSize: 100
|
|
12
|
-
}
|
|
19
|
+
const ITEM_SIZE = 36 // 32px item height + 4px margin-bottom
|
|
20
|
+
const OVERSCAN = 5
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
page
|
|
17
|
-
})
|
|
18
|
-
}
|
|
22
|
+
export default class VirtualList extends Component {
|
|
23
|
+
rootRef = createRef()
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
render () {
|
|
26
|
+
const { list, renderItem, containerHeight = 400, scrollTop = 0 } = this.props
|
|
27
|
+
|
|
28
|
+
// offsetTop: distance from scroll container top to this list's top.
|
|
29
|
+
// rootRef.offsetTop is relative to the nearest positioned ancestor, which is
|
|
30
|
+
// .sftp-table-content (position: relative) — exactly the scroll container.
|
|
31
|
+
// Returns 0 on first render (rootRef not yet set); harmless (renders a few extra items).
|
|
32
|
+
const offsetTop = this.rootRef.current?.offsetTop ?? 0
|
|
33
|
+
|
|
34
|
+
const startIndex = Math.max(
|
|
35
|
+
0,
|
|
36
|
+
Math.floor((scrollTop - offsetTop) / ITEM_SIZE) - OVERSCAN
|
|
37
|
+
)
|
|
38
|
+
const endIndex = Math.min(
|
|
39
|
+
list.length - 1,
|
|
40
|
+
Math.ceil((scrollTop + containerHeight - offsetTop) / ITEM_SIZE) + OVERSCAN
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const topSpacerHeight = startIndex * ITEM_SIZE
|
|
44
|
+
const bottomSpacerHeight = Math.max(0, (list.length - endIndex - 1) * ITEM_SIZE)
|
|
33
45
|
|
|
34
|
-
renderPager () {
|
|
35
|
-
const props = {
|
|
36
|
-
current: this.state.page,
|
|
37
|
-
pageSize: this.state.pageSize,
|
|
38
|
-
total: this.props.list.length,
|
|
39
|
-
showLessItems: true,
|
|
40
|
-
showSizeChanger: false,
|
|
41
|
-
simple: false,
|
|
42
|
-
onChange: this.onChange
|
|
43
|
-
}
|
|
44
46
|
return (
|
|
45
|
-
<div
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
<div ref={this.rootRef}>
|
|
48
|
+
<div style={{ height: topSpacerHeight }} />
|
|
49
|
+
{list.slice(startIndex, endIndex + 1).map((item, i) =>
|
|
50
|
+
renderItem(item, startIndex + i)
|
|
51
|
+
)}
|
|
52
|
+
<div style={{ height: bottomSpacerHeight }} />
|
|
49
53
|
</div>
|
|
50
54
|
)
|
|
51
55
|
}
|
|
52
|
-
|
|
53
|
-
render () {
|
|
54
|
-
return this.renderList()
|
|
55
|
-
}
|
|
56
56
|
}
|
|
@@ -970,7 +970,7 @@ export default class Sftp extends Component {
|
|
|
970
970
|
})
|
|
971
971
|
}
|
|
972
972
|
|
|
973
|
-
parsePath = (type, pth) => {
|
|
973
|
+
parsePath = async (type, pth) => {
|
|
974
974
|
const reg = /^%([^%]+)%/
|
|
975
975
|
if (!reg.test(pth)) {
|
|
976
976
|
return pth
|
|
@@ -980,13 +980,14 @@ export default class Sftp extends Component {
|
|
|
980
980
|
return pth
|
|
981
981
|
}
|
|
982
982
|
const envName = m[1]
|
|
983
|
-
const envPath = window.pre.
|
|
983
|
+
const envPath = await window.pre.runGlobalAsync('getEnv', envName)
|
|
984
984
|
if (envPath) {
|
|
985
985
|
return pth.replace(reg, envPath)
|
|
986
986
|
}
|
|
987
|
+
return pth
|
|
987
988
|
}
|
|
988
989
|
|
|
989
|
-
onGoto = (type, e) => {
|
|
990
|
+
onGoto = async (type, e) => {
|
|
990
991
|
e && e.preventDefault()
|
|
991
992
|
if (type === typeMap.remote && !this.sftp) {
|
|
992
993
|
return this.initData(true)
|
|
@@ -994,7 +995,7 @@ export default class Sftp extends Component {
|
|
|
994
995
|
const n = `${type}Path`
|
|
995
996
|
const nt = n + 'Temp'
|
|
996
997
|
const oldPath = this.state[type + 'Path']
|
|
997
|
-
const np = this.parsePath(type, this.state[nt])
|
|
998
|
+
const np = await this.parsePath(type, this.state[nt])
|
|
998
999
|
if (!isValidPath(np)) {
|
|
999
1000
|
return notification.warning({
|
|
1000
1001
|
message: 'path not valid'
|