@electerm/electerm-react 3.8.15 → 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/pre.js +0 -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 +3 -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 +5 -5
- package/client/components/widgets/widget-control.jsx +1 -0
- package/client/store/common.js +1 -1
- 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/terminal/socket-close-warning.jsx +0 -94
package/client/common/pre.js
CHANGED
|
@@ -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} />)
|
|
@@ -36,6 +36,7 @@ import WorkspaceSaveModal from '../tabs/workspace-save-modal'
|
|
|
36
36
|
import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
|
|
37
37
|
import AutoSync from '../setting-sync/auto-sync'
|
|
38
38
|
import BatchOpRunner from '../batch-op/batch-op-runner'
|
|
39
|
+
import UnixTimestampTooltip from '../terminal/unix-timestamp-tooltip'
|
|
39
40
|
import { pick } from 'lodash-es'
|
|
40
41
|
import deepCopy from 'json-deep-copy'
|
|
41
42
|
import './wrapper.styl'
|
|
@@ -51,7 +52,7 @@ export default auto(function Index (props) {
|
|
|
51
52
|
ipcOnEvent('open-about', store.openAbout)
|
|
52
53
|
ipcOnEvent('new-ssh', store.onNewSsh)
|
|
53
54
|
ipcOnEvent('add-tab-from-command-line', store.addTabFromCommandLine)
|
|
54
|
-
ipcOnEvent('open-tab', (e, parsed) => store.
|
|
55
|
+
ipcOnEvent('open-tab', (e, parsed) => store.ipcOpenTab(parsed))
|
|
55
56
|
ipcOnEvent('openSettings', store.openSetting)
|
|
56
57
|
ipcOnEvent('selectall', store.selectall)
|
|
57
58
|
ipcOnEvent('focused', store.focus)
|
|
@@ -297,6 +298,7 @@ export default auto(function Index (props) {
|
|
|
297
298
|
<NotificationContainer />
|
|
298
299
|
<BatchOpRunner />
|
|
299
300
|
<AIConfigModal store={store} />
|
|
301
|
+
<UnixTimestampTooltip />
|
|
300
302
|
</div>
|
|
301
303
|
</ConfigProvider>
|
|
302
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'
|
|
@@ -16,6 +16,7 @@ import Link from '../common/external-link'
|
|
|
16
16
|
import LogoElem from '../common/logo-elem'
|
|
17
17
|
import RunningTime from './app-running-time'
|
|
18
18
|
import { auto } from 'manate/react'
|
|
19
|
+
import { useState } from 'react'
|
|
19
20
|
|
|
20
21
|
import {
|
|
21
22
|
packInfo,
|
|
@@ -27,8 +28,13 @@ import './info.styl'
|
|
|
27
28
|
const e = window.translate
|
|
28
29
|
|
|
29
30
|
export default auto(function InfoModal (props) {
|
|
31
|
+
const [runtimeEnv, setRuntimeEnv] = useState(null)
|
|
32
|
+
|
|
30
33
|
const handleChangeTab = key => {
|
|
31
34
|
window.store.infoModalTab = key
|
|
35
|
+
if (key === infoTabs.env && !runtimeEnv) {
|
|
36
|
+
window.pre.runGlobalAsync('getEnv').then(env => setRuntimeEnv(env))
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
const renderCheckUpdate = () => {
|
|
@@ -135,14 +141,14 @@ export default auto(function InfoModal (props) {
|
|
|
135
141
|
knownIssuesLink
|
|
136
142
|
} = packInfo
|
|
137
143
|
const link = releaseLink.replace('/releases', '')
|
|
138
|
-
const {
|
|
144
|
+
const { versions } = window.pre
|
|
139
145
|
const deps = {
|
|
140
146
|
...devDependencies,
|
|
141
147
|
...dependencies
|
|
142
148
|
}
|
|
143
149
|
const envs = {
|
|
144
150
|
...versions,
|
|
145
|
-
...
|
|
151
|
+
...(runtimeEnv || {})
|
|
146
152
|
}
|
|
147
153
|
const title = (
|
|
148
154
|
<div className='custom-modal-close-confirm-title font16'>
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
import { memo } from 'react'
|
|
2
|
-
import { Button } from 'antd'
|
|
3
|
-
import { LoadingOutlined } from '@ant-design/icons'
|
|
4
2
|
|
|
5
3
|
const e = window.translate
|
|
6
4
|
|
|
7
|
-
export default memo(function ReconnectOverlay ({ countdown
|
|
5
|
+
export default memo(function ReconnectOverlay ({ countdown }) {
|
|
8
6
|
if (countdown === null || countdown === undefined) {
|
|
9
7
|
return null
|
|
10
8
|
}
|
|
11
9
|
return (
|
|
12
10
|
<div className='terminal-reconnect-overlay'>
|
|
13
|
-
|
|
14
|
-
<LoadingOutlined className='terminal-reconnect-icon' />
|
|
15
|
-
<div className='terminal-reconnect-msg'>
|
|
16
|
-
{e('autoReconnectTerminal')}: {countdown}s
|
|
17
|
-
</div>
|
|
18
|
-
<Button
|
|
19
|
-
size='small'
|
|
20
|
-
onClick={onCancel}
|
|
21
|
-
>
|
|
22
|
-
{e('cancel')}
|
|
23
|
-
</Button>
|
|
24
|
-
</div>
|
|
11
|
+
{e('autoReconnectTerminal')}: {countdown}s
|
|
25
12
|
</div>
|
|
26
13
|
)
|
|
27
14
|
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Alert
|
|
5
|
+
} from 'antd'
|
|
6
|
+
|
|
7
|
+
const e = window.translate
|
|
8
|
+
|
|
9
|
+
export default memo(function TerminalErrorHandle ({
|
|
10
|
+
errorMessage,
|
|
11
|
+
showEditBookmarkButton,
|
|
12
|
+
onEditBookmark
|
|
13
|
+
}) {
|
|
14
|
+
if (!errorMessage) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderEditBookmarkButton () {
|
|
19
|
+
if (!showEditBookmarkButton) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
<div className='terminal-error-actions pd1y'>
|
|
24
|
+
<Button
|
|
25
|
+
onClick={onEditBookmark}
|
|
26
|
+
>
|
|
27
|
+
{e('edit')} {e('bookmarks')}
|
|
28
|
+
</Button>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Alert
|
|
35
|
+
className='terminal-error-handle'
|
|
36
|
+
message={errorMessage}
|
|
37
|
+
type='error'
|
|
38
|
+
showIcon
|
|
39
|
+
banner
|
|
40
|
+
description={renderEditBookmarkButton()}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Component, createRef } from 'react'
|
|
2
|
-
import { handleErr } from '../../common/fetch.jsx'
|
|
3
2
|
import { isEqual, pick, debounce, throttle } from 'lodash-es'
|
|
4
3
|
import clone from '../../common/to-simple-obj.js'
|
|
5
4
|
import resolve from '../../common/resolve.js'
|
|
@@ -7,7 +6,6 @@ import {
|
|
|
7
6
|
Spin,
|
|
8
7
|
Dropdown
|
|
9
8
|
} from 'antd'
|
|
10
|
-
import { notification } from '../common/notification'
|
|
11
9
|
import message from '../common/message'
|
|
12
10
|
import Modal from '../common/modal'
|
|
13
11
|
import classnames from 'classnames'
|
|
@@ -48,8 +46,8 @@ import ExternalLink from '../common/external-link.jsx'
|
|
|
48
46
|
import createDefaultLogPath from '../../common/default-log-path.js'
|
|
49
47
|
import SearchResultBar from './terminal-search-bar'
|
|
50
48
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
51
|
-
import { showSocketCloseWarning } from './socket-close-warning.jsx'
|
|
52
49
|
import ReconnectOverlay from './reconnect-overlay.jsx'
|
|
50
|
+
import TerminalErrorHandle from './terminal-error-handle.jsx'
|
|
53
51
|
import {
|
|
54
52
|
loadTerminal,
|
|
55
53
|
loadFitAddon,
|
|
@@ -78,6 +76,7 @@ class Term extends Component {
|
|
|
78
76
|
matchIndex: -1,
|
|
79
77
|
totalLines: 0,
|
|
80
78
|
reconnectCountdown: null,
|
|
79
|
+
terminalError: null,
|
|
81
80
|
dropFileModalVisible: false,
|
|
82
81
|
droppedFiles: []
|
|
83
82
|
}
|
|
@@ -169,11 +168,6 @@ class Term extends Component {
|
|
|
169
168
|
this.fitAddon = null
|
|
170
169
|
this.cmdAddon = null
|
|
171
170
|
this.imageAddon = null
|
|
172
|
-
// Clear the notification if it exists
|
|
173
|
-
if (this.socketCloseWarning) {
|
|
174
|
-
notification.destroy(this.socketCloseWarning.key)
|
|
175
|
-
this.socketCloseWarning = null
|
|
176
|
-
}
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
terminalConfigProps = [
|
|
@@ -958,9 +952,10 @@ class Term extends Component {
|
|
|
958
952
|
}
|
|
959
953
|
|
|
960
954
|
onSelectionChange = () => {
|
|
961
|
-
this.
|
|
962
|
-
|
|
963
|
-
})
|
|
955
|
+
const hasSelection = this.term.hasSelection()
|
|
956
|
+
const txt = hasSelection ? this.term.getSelection().trim() : ''
|
|
957
|
+
this.setState({ hasSelection })
|
|
958
|
+
refsStatic.get('unix-timestamp-tooltip')?.onSelection(txt)
|
|
964
959
|
}
|
|
965
960
|
|
|
966
961
|
// setActive = () => {
|
|
@@ -1170,7 +1165,8 @@ class Term extends Component {
|
|
|
1170
1165
|
|
|
1171
1166
|
remoteInit = async (term = this.term) => {
|
|
1172
1167
|
this.setState({
|
|
1173
|
-
loading: true
|
|
1168
|
+
loading: true,
|
|
1169
|
+
terminalError: null
|
|
1174
1170
|
})
|
|
1175
1171
|
const { cols, rows } = term
|
|
1176
1172
|
const { config } = this.props
|
|
@@ -1248,7 +1244,7 @@ class Term extends Component {
|
|
|
1248
1244
|
.catch(err => {
|
|
1249
1245
|
if (!isAutoReconnect) {
|
|
1250
1246
|
const text = err.message
|
|
1251
|
-
|
|
1247
|
+
this.handleError({ message: text, from, srcId })
|
|
1252
1248
|
}
|
|
1253
1249
|
})
|
|
1254
1250
|
if (typeof r === 'string' && r.includes('fail')) {
|
|
@@ -1304,6 +1300,29 @@ class Term extends Component {
|
|
|
1304
1300
|
)
|
|
1305
1301
|
}
|
|
1306
1302
|
|
|
1303
|
+
handleError = ({ message: errorMessage, from, srcId }) => {
|
|
1304
|
+
this.setState({
|
|
1305
|
+
terminalError: {
|
|
1306
|
+
message: errorMessage || 'Failed to create terminal session',
|
|
1307
|
+
from,
|
|
1308
|
+
srcId
|
|
1309
|
+
}
|
|
1310
|
+
})
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
handleEditBookmarkFromError = () => {
|
|
1314
|
+
const error = this.state.terminalError
|
|
1315
|
+
if (!error || error.from !== 'bookmarks' || !error.srcId) {
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
const item = window.store.bookmarksMap?.get(error.srcId) ||
|
|
1319
|
+
window.store.bookmarks?.find(d => d.id === error.srcId)
|
|
1320
|
+
if (!item) {
|
|
1321
|
+
return
|
|
1322
|
+
}
|
|
1323
|
+
window.store.openBookmarkEdit(item)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1307
1326
|
initSocketEvents = () => {
|
|
1308
1327
|
const originalSend = this.socket.send
|
|
1309
1328
|
this.socket.send = (data) => {
|
|
@@ -1369,31 +1388,8 @@ class Term extends Component {
|
|
|
1369
1388
|
return this.props.delTab(this.props.tab.id)
|
|
1370
1389
|
}
|
|
1371
1390
|
const { autoReconnectTerminal } = this.props.config
|
|
1372
|
-
const isActive = this.isActiveTerminal()
|
|
1373
|
-
const isFocused = window.focused
|
|
1374
1391
|
if (autoReconnectTerminal) {
|
|
1375
|
-
|
|
1376
|
-
this.socketCloseWarning = showSocketCloseWarning({
|
|
1377
|
-
tabId: this.props.tab.id,
|
|
1378
|
-
tab: this.props.tab,
|
|
1379
|
-
autoReconnect: true,
|
|
1380
|
-
delTab: this.props.delTab,
|
|
1381
|
-
reloadTab: this.props.reloadTab
|
|
1382
|
-
})
|
|
1383
|
-
} else {
|
|
1384
|
-
this.scheduleAutoReconnect(3000)
|
|
1385
|
-
}
|
|
1386
|
-
} else {
|
|
1387
|
-
if (!isActive || !isFocused) {
|
|
1388
|
-
return false
|
|
1389
|
-
}
|
|
1390
|
-
this.socketCloseWarning = showSocketCloseWarning({
|
|
1391
|
-
tabId: this.props.tab.id,
|
|
1392
|
-
tab: this.props.tab,
|
|
1393
|
-
autoReconnect: false,
|
|
1394
|
-
delTab: this.props.delTab,
|
|
1395
|
-
reloadTab: this.props.reloadTab
|
|
1396
|
-
})
|
|
1392
|
+
this.scheduleAutoReconnect(3000)
|
|
1397
1393
|
}
|
|
1398
1394
|
}
|
|
1399
1395
|
|
|
@@ -1547,9 +1543,13 @@ class Term extends Component {
|
|
|
1547
1543
|
<RemoteFloatControl
|
|
1548
1544
|
isFullScreen={fullscreen}
|
|
1549
1545
|
/>
|
|
1546
|
+
<TerminalErrorHandle
|
|
1547
|
+
errorMessage={this.state.terminalError?.message}
|
|
1548
|
+
showEditBookmarkButton={this.state.terminalError?.from === 'bookmarks' && !!this.state.terminalError?.srcId}
|
|
1549
|
+
onEditBookmark={this.handleEditBookmarkFromError}
|
|
1550
|
+
/>
|
|
1550
1551
|
<ReconnectOverlay
|
|
1551
1552
|
countdown={this.state.reconnectCountdown}
|
|
1552
|
-
onCancel={this.handleCancelAutoReconnect}
|
|
1553
1553
|
/>
|
|
1554
1554
|
<DropFileModal
|
|
1555
1555
|
visible={this.state.dropFileModalVisible}
|
|
@@ -153,12 +153,17 @@
|
|
|
153
153
|
|
|
154
154
|
.terminal-reconnect-overlay
|
|
155
155
|
position absolute
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
align-items center
|
|
162
|
-
justify-content center
|
|
156
|
+
bottom 6px
|
|
157
|
+
right 12px
|
|
158
|
+
font-size 11px
|
|
159
|
+
color rgba(255, 255, 255, 0.55)
|
|
160
|
+
pointer-events none
|
|
163
161
|
z-index 110
|
|
164
162
|
|
|
163
|
+
.terminal-error-handle
|
|
164
|
+
position absolute
|
|
165
|
+
left 0
|
|
166
|
+
top 0
|
|
167
|
+
z-index 100
|
|
168
|
+
background var(--main)
|
|
169
|
+
max-width calc(100% - 24px)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global tooltip that detects Unix timestamps in terminal selections
|
|
3
|
+
* and displays their human-readable date/time near the cursor.
|
|
4
|
+
* Registered via refsStatic as 'unix-timestamp-tooltip'.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Component } from 'react'
|
|
8
|
+
import { refsStatic } from '../common/ref'
|
|
9
|
+
|
|
10
|
+
export default class UnixTimestampTooltip extends Component {
|
|
11
|
+
state = {
|
|
12
|
+
visible: false,
|
|
13
|
+
x: 0,
|
|
14
|
+
y: 0,
|
|
15
|
+
text: ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_mouseX = 0
|
|
19
|
+
_mouseY = 0
|
|
20
|
+
|
|
21
|
+
componentDidMount () {
|
|
22
|
+
refsStatic.add('unix-timestamp-tooltip', this)
|
|
23
|
+
document.addEventListener('mousemove', this.onMouseMove)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
componentWillUnmount () {
|
|
27
|
+
refsStatic.remove('unix-timestamp-tooltip')
|
|
28
|
+
document.removeEventListener('mousemove', this.onMouseMove)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
detectUnixTimestamp (txt) {
|
|
32
|
+
if (!/^\d+$/.test(txt)) return null
|
|
33
|
+
const num = parseInt(txt, 10)
|
|
34
|
+
// seconds: 9-10 digits, year ~2001-2286
|
|
35
|
+
if ((txt.length === 9 || txt.length === 10) && num >= 946684800 && num <= 32503680000) {
|
|
36
|
+
return new Date(num * 1000).toLocaleString()
|
|
37
|
+
}
|
|
38
|
+
// milliseconds: 13 digits
|
|
39
|
+
if (txt.length === 13 && num >= 946684800000 && num <= 32503680000000) {
|
|
40
|
+
return new Date(num).toLocaleString()
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onMouseMove = (e) => {
|
|
46
|
+
this._mouseX = e.clientX
|
|
47
|
+
this._mouseY = e.clientY
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onSelection = (txt) => {
|
|
51
|
+
const ts = this.detectUnixTimestamp(txt)
|
|
52
|
+
if (ts) {
|
|
53
|
+
this.setState({ visible: true, x: this._mouseX, y: this._mouseY, text: ts })
|
|
54
|
+
} else {
|
|
55
|
+
this.setState({ visible: false })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render () {
|
|
60
|
+
const { visible, x, y, text } = this.state
|
|
61
|
+
if (!visible) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
position: 'fixed',
|
|
68
|
+
left: x,
|
|
69
|
+
top: y - 36,
|
|
70
|
+
background: 'rgba(0,0,0,0.75)',
|
|
71
|
+
color: '#fff',
|
|
72
|
+
padding: '3px 8px',
|
|
73
|
+
borderRadius: 4,
|
|
74
|
+
fontSize: 12,
|
|
75
|
+
whiteSpace: 'nowrap',
|
|
76
|
+
pointerEvents: 'none',
|
|
77
|
+
transform: 'translateX(-50%)',
|
|
78
|
+
zIndex: 9999
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{text}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
MenuOutlined,
|
|
8
8
|
EditOutlined
|
|
9
9
|
} from '@ant-design/icons'
|
|
10
|
-
import { Button, Space, Dropdown } from 'antd'
|
|
10
|
+
import { Button, Space, Dropdown, Flex } from 'antd'
|
|
11
11
|
import copy from 'json-deep-copy'
|
|
12
12
|
import time from '../../common/time'
|
|
13
13
|
import download from '../../common/download'
|
|
@@ -87,8 +87,8 @@ export default function BookmarkToolbar (props) {
|
|
|
87
87
|
return (
|
|
88
88
|
|
|
89
89
|
<div className='pd1b pd1r'>
|
|
90
|
-
<
|
|
91
|
-
<div
|
|
90
|
+
<Flex justify='space-between' align='center'>
|
|
91
|
+
<div>
|
|
92
92
|
<Space.Compact>
|
|
93
93
|
<Button onClick={onNewBookmark}>
|
|
94
94
|
<BookOutlined className='with-plus' />
|
|
@@ -122,12 +122,12 @@ export default function BookmarkToolbar (props) {
|
|
|
122
122
|
</Button>
|
|
123
123
|
</Space.Compact>
|
|
124
124
|
</div>
|
|
125
|
-
<div
|
|
125
|
+
<div>
|
|
126
126
|
<Dropdown {...ddProps}>
|
|
127
127
|
<MenuOutlined />
|
|
128
128
|
</Dropdown>
|
|
129
129
|
</div>
|
|
130
|
-
</
|
|
130
|
+
</Flex>
|
|
131
131
|
</div>
|
|
132
132
|
)
|
|
133
133
|
}
|
|
@@ -55,6 +55,7 @@ export default function WidgetControl ({ formData, widgetInstancesLength }) {
|
|
|
55
55
|
showMsg(msg, 'success', result.serverInfo, 10)
|
|
56
56
|
} catch (err) {
|
|
57
57
|
console.error('Failed to run widget:', err)
|
|
58
|
+
showMsg(`Failed to run widget: ${err.message}`, 'error', null, 10)
|
|
58
59
|
} finally {
|
|
59
60
|
setLoading(false)
|
|
60
61
|
}
|
package/client/store/common.js
CHANGED
|
@@ -298,7 +298,7 @@ export default Store => {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
Store.prototype.aiConfigMissing = function () {
|
|
301
|
-
return aiConfigsArr.
|
|
301
|
+
return aiConfigsArr.filter(k => k !== 'apiKeyAI' && k !== 'proxyAI').some(k => !window.store.config[k])
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
Store.prototype.clearHistory = function () {
|
|
@@ -100,7 +100,7 @@ export async function addTabFromCommandLine (store, opts) {
|
|
|
100
100
|
(conf.username && conf.host) ||
|
|
101
101
|
conf.fromCmdLine
|
|
102
102
|
) {
|
|
103
|
-
store.
|
|
103
|
+
store.ipcOpenTab(conf)
|
|
104
104
|
} else if (
|
|
105
105
|
options.initFolder &&
|
|
106
106
|
!(store.config.onStartSessions || []).length &&
|
|
@@ -229,7 +229,7 @@ export default (Store) => {
|
|
|
229
229
|
Store.prototype.checkPendingDeepLink = async function () {
|
|
230
230
|
const pending = await window.pre.runGlobalAsync('getPendingDeepLink')
|
|
231
231
|
if (pending) {
|
|
232
|
-
window.store.
|
|
232
|
+
window.store.ipcOpenTab(pending)
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
Store.prototype.parseQuickConnect = function (url) {
|
package/client/store/sync.js
CHANGED
|
@@ -68,11 +68,12 @@ export default (Store) => {
|
|
|
68
68
|
).join('####')
|
|
69
69
|
}
|
|
70
70
|
if (type === syncTypes.webdav) {
|
|
71
|
-
// WebDAV token format: serverUrl####username####password
|
|
71
|
+
// WebDAV token format: serverUrl####username####password####skipVerify
|
|
72
72
|
const serverUrl = get(window.store.config, 'syncSetting.webdavServerUrl')
|
|
73
73
|
const username = get(window.store.config, 'syncSetting.webdavUsername')
|
|
74
74
|
const password = get(window.store.config, 'syncSetting.webdavPassword')
|
|
75
|
-
|
|
75
|
+
const skipVerify = get(window.store.config, 'syncSetting.webdavSkipVerify') || false
|
|
76
|
+
return [serverUrl, username, password, skipVerify].join('####')
|
|
76
77
|
}
|
|
77
78
|
return get(window.store.config, 'syncSetting.' + type + 'AccessToken')
|
|
78
79
|
}
|
package/client/store/tab.js
CHANGED
|
@@ -362,6 +362,26 @@ export default Store => {
|
|
|
362
362
|
store.updateHistory(newTab)
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// Dangerous props that should not be accepted from IPC
|
|
366
|
+
const dangerousTabProps = [
|
|
367
|
+
'execLinux',
|
|
368
|
+
'execMac',
|
|
369
|
+
'execWindows',
|
|
370
|
+
'execWindowsArgs',
|
|
371
|
+
'execMacArgs',
|
|
372
|
+
'execLinuxArgs',
|
|
373
|
+
'setEnv',
|
|
374
|
+
'runScripts',
|
|
375
|
+
'interactiveValues'
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
Store.prototype.ipcOpenTab = function (parsed) {
|
|
379
|
+
const safeTab = Object.fromEntries(
|
|
380
|
+
Object.entries(parsed).filter(([key]) => !dangerousTabProps.includes(key))
|
|
381
|
+
)
|
|
382
|
+
return window.store.addTab(safeTab)
|
|
383
|
+
}
|
|
384
|
+
|
|
365
385
|
Store.prototype.clickNextTab = debounce(function () {
|
|
366
386
|
window.store.clickBioTab(1)
|
|
367
387
|
}, 100)
|
package/package.json
CHANGED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react'
|
|
2
|
-
import { Button } from 'antd'
|
|
3
|
-
import { ReloadOutlined } from '@ant-design/icons'
|
|
4
|
-
import { notification } from '../common/notification'
|
|
5
|
-
|
|
6
|
-
const e = window.translate
|
|
7
|
-
const COUNTDOWN_SECONDS = 3
|
|
8
|
-
|
|
9
|
-
export function showSocketCloseWarning ({
|
|
10
|
-
tabId,
|
|
11
|
-
tab,
|
|
12
|
-
autoReconnect,
|
|
13
|
-
delTab,
|
|
14
|
-
reloadTab
|
|
15
|
-
}) {
|
|
16
|
-
const key = `open${Date.now()}`
|
|
17
|
-
|
|
18
|
-
function closeNotification () {
|
|
19
|
-
notification.destroy(key)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const descriptionNode = (
|
|
23
|
-
<SocketCloseDescription
|
|
24
|
-
autoReconnect={autoReconnect}
|
|
25
|
-
onClose={() => {
|
|
26
|
-
closeNotification()
|
|
27
|
-
delTab(tabId)
|
|
28
|
-
}}
|
|
29
|
-
onReload={() => {
|
|
30
|
-
closeNotification()
|
|
31
|
-
reloadTab({ ...tab, autoReConnect: (tab.autoReConnect || 0) + 1 })
|
|
32
|
-
}}
|
|
33
|
-
/>
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
notification.warning({
|
|
37
|
-
key,
|
|
38
|
-
message: e('socketCloseTip'),
|
|
39
|
-
duration: autoReconnect ? COUNTDOWN_SECONDS + 2 : 30,
|
|
40
|
-
description: descriptionNode
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
return { key }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function SocketCloseDescription ({ autoReconnect, onClose, onReload }) {
|
|
47
|
-
const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS)
|
|
48
|
-
const timerRef = useRef(null)
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!autoReconnect) {
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
timerRef.current = setInterval(() => {
|
|
55
|
-
setCountdown(prev => {
|
|
56
|
-
if (prev <= 1) {
|
|
57
|
-
clearInterval(timerRef.current)
|
|
58
|
-
onReload()
|
|
59
|
-
return 0
|
|
60
|
-
}
|
|
61
|
-
return prev - 1
|
|
62
|
-
})
|
|
63
|
-
}, 1000)
|
|
64
|
-
|
|
65
|
-
return () => {
|
|
66
|
-
if (timerRef.current) {
|
|
67
|
-
clearInterval(timerRef.current)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}, [autoReconnect])
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className='pd2y'>
|
|
74
|
-
{autoReconnect && (
|
|
75
|
-
<div className='pd1b'>
|
|
76
|
-
{e('autoReconnectTerminal')}: {countdown}s
|
|
77
|
-
</div>
|
|
78
|
-
)}
|
|
79
|
-
<Button
|
|
80
|
-
className='mg1r'
|
|
81
|
-
type='primary'
|
|
82
|
-
onClick={onClose}
|
|
83
|
-
>
|
|
84
|
-
{e('close')}
|
|
85
|
-
</Button>
|
|
86
|
-
<Button
|
|
87
|
-
icon={<ReloadOutlined />}
|
|
88
|
-
onClick={onReload}
|
|
89
|
-
>
|
|
90
|
-
{e('reload')}
|
|
91
|
-
</Button>
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
}
|