@electerm/electerm-react 3.0.6 → 3.0.18

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.
@@ -72,6 +72,7 @@ export default {
72
72
  sessionLogPath: '',
73
73
  sshSftpSplitView: false,
74
74
  showCmdSuggestions: false,
75
+ autoReconnectTerminal: false,
75
76
  startDirectoryLocal: '',
76
77
  allowMultiInstance: false,
77
78
  disableDeveloperTool: false
@@ -34,6 +34,7 @@ const ftpConfig = {
34
34
  { type: 'password', name: 'password', label: () => e('password') },
35
35
  { type: 'switch', name: 'secure', label: () => e('secure'), valuePropName: 'checked' },
36
36
  commonFields.encode,
37
+ commonFields.proxy,
37
38
  commonFields.type
38
39
  ]
39
40
  }
@@ -11,6 +11,7 @@ import pixed from '../layout/pixed'
11
11
  export default class Sessions extends Component {
12
12
  // Function to reload a tab using store.reloadTab
13
13
  reloadTab = (tab) => {
14
+ window.store.updateTab(tab.id, tab)
14
15
  window.store.reloadTab(tab.id)
15
16
  }
16
17
 
@@ -589,7 +589,8 @@ export default class SettingTerminal extends Component {
589
589
  'ctrlOrMetaOpenTerminalLink',
590
590
  'sftpPathFollowSsh',
591
591
  'sshSftpSplitView',
592
- 'showCmdSuggestions'
592
+ 'showCmdSuggestions',
593
+ 'autoReconnectTerminal'
593
594
  ].map(d => this.renderToggle(d))
594
595
  }
595
596
  <div className='pd1b'>{e('terminalBackSpaceMode')}</div>
@@ -68,7 +68,7 @@ export default class AddrBookmarkItem extends Component {
68
68
  onDrop={this.handleDrop}
69
69
  >
70
70
  {globTag}
71
- <b>{item.addr}</b>
71
+ <b className='mg1l'>{item.addr}</b>
72
72
  <CloseCircleOutlined
73
73
  className='del-addr-bookmark'
74
74
  onClick={this.handleDel}
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react'
1
2
  import { auto } from 'manate/react'
2
3
  import {
3
4
  StarOutlined,
@@ -13,6 +14,8 @@ import uid from '../../common/uid'
13
14
  import './address-bookmark.styl'
14
15
 
15
16
  export default auto(function AddrBookmark (props) {
17
+ const [open, setOpen] = useState(false)
18
+
16
19
  function onDel (item) {
17
20
  window.store.delAddressBookmark(item)
18
21
  }
@@ -39,6 +42,11 @@ export default auto(function AddrBookmark (props) {
39
42
  handleAddAddrAct(true)
40
43
  }
41
44
 
45
+ function handleClick (type, addr) {
46
+ setOpen(false)
47
+ onClickHistory(type, addr)
48
+ }
49
+
42
50
  const { type, onClickHistory, host } = props
43
51
  const { store } = window
44
52
  // const cls = classnames(
@@ -58,7 +66,7 @@ export default auto(function AddrBookmark (props) {
58
66
  ? addrs.map(o => {
59
67
  return (
60
68
  <AddrBookmarkItem
61
- handleClick={onClickHistory}
69
+ handleClick={handleClick}
62
70
  type={type}
63
71
  key={o.id}
64
72
  handleDel={onDel}
@@ -100,6 +108,8 @@ export default auto(function AddrBookmark (props) {
100
108
  title={title}
101
109
  placement='bottom'
102
110
  trigger='click'
111
+ open={open}
112
+ onOpenChange={setOpen}
103
113
  >
104
114
  <StarOutlined className={props.className || ''} />
105
115
  </Popover>
@@ -998,7 +998,7 @@ export default class FileSection extends React.Component {
998
998
  const shouldShowSelectedMenu = id &&
999
999
  len > 1 &&
1000
1000
  selectedFiles.has(id)
1001
- const delTxt = shouldShowSelectedMenu ? `${e('deleteAll')}(${len})` : e('del')
1001
+ const delTxt = shouldShowSelectedMenu ? `${e('del')}:${e('selected')}(${len})` : e('del')
1002
1002
  const canPaste = hasFileInClipboardText()
1003
1003
  const showEdit = !isDirectory && id &&
1004
1004
  size < maxEditFileSize
@@ -167,7 +167,7 @@
167
167
  z-index 900
168
168
  border-radius 0 0 3px 3px
169
169
  padding 0
170
- background var(--main-lighter)
170
+ background var(--main)
171
171
 
172
172
  .window-control-box
173
173
  display inline-block
@@ -0,0 +1,27 @@
1
+ import { memo } from 'react'
2
+ import { Button } from 'antd'
3
+ import { LoadingOutlined } from '@ant-design/icons'
4
+
5
+ const e = window.translate
6
+
7
+ export default memo(function ReconnectOverlay ({ countdown, onCancel }) {
8
+ if (countdown === null || countdown === undefined) {
9
+ return null
10
+ }
11
+ return (
12
+ <div className='terminal-reconnect-overlay'>
13
+ <div className='terminal-reconnect-box'>
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>
25
+ </div>
26
+ )
27
+ })
@@ -0,0 +1,94 @@
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
+ }
@@ -3,12 +3,8 @@ import { handleErr } from '../../common/fetch.jsx'
3
3
  import { isEqual, pick, debounce, throttle } from 'lodash-es'
4
4
  import clone from '../../common/to-simple-obj.js'
5
5
  import resolve from '../../common/resolve.js'
6
- import {
7
- ReloadOutlined
8
- } from '@ant-design/icons'
9
6
  import {
10
7
  Spin,
11
- Button,
12
8
  Dropdown
13
9
  } from 'antd'
14
10
  import { notification } from '../common/notification'
@@ -50,6 +46,8 @@ import ExternalLink from '../common/external-link.jsx'
50
46
  import createDefaultLogPath from '../../common/default-log-path.js'
51
47
  import SearchResultBar from './terminal-search-bar'
52
48
  import RemoteFloatControl from '../common/remote-float-control'
49
+ import { showSocketCloseWarning } from './socket-close-warning.jsx'
50
+ import ReconnectOverlay from './reconnect-overlay.jsx'
53
51
  import {
54
52
  loadTerminal,
55
53
  loadFitAddon,
@@ -76,7 +74,8 @@ class Term extends Component {
76
74
  lines: [],
77
75
  searchResults: [],
78
76
  matchIndex: -1,
79
- totalLines: 0
77
+ totalLines: 0,
78
+ reconnectCountdown: null
80
79
  }
81
80
  this.id = `term-${this.props.tab.id}`
82
81
  refs.add(this.id, this)
@@ -1139,10 +1138,13 @@ class Term extends Component {
1139
1138
  ? typeMap.remote
1140
1139
  : typeMap.local
1141
1140
  })
1141
+ const isAutoReconnect = !!(tab.autoReConnect && this.props.config.autoReconnectTerminal)
1142
1142
  const r = await createTerm(opts)
1143
1143
  .catch(err => {
1144
- const text = err.message
1145
- handleErr({ message: text })
1144
+ if (!isAutoReconnect) {
1145
+ const text = err.message
1146
+ handleErr({ message: text })
1147
+ }
1146
1148
  })
1147
1149
  if (typeof r === 'string' && r.includes('fail')) {
1148
1150
  return this.promote()
@@ -1154,6 +1156,10 @@ class Term extends Component {
1154
1156
  loading: false
1155
1157
  })
1156
1158
  if (!r) {
1159
+ if (isAutoReconnect) {
1160
+ this.scheduleAutoReconnect(3000)
1161
+ return
1162
+ }
1157
1163
  this.setStatus(statusMap.error)
1158
1164
  return
1159
1165
  }
@@ -1254,46 +1260,70 @@ class Term extends Component {
1254
1260
  this.setStatus(
1255
1261
  statusMap.error
1256
1262
  )
1257
- if (!this.isActiveTerminal() || !window.focused) {
1258
- return false
1259
- }
1260
1263
  if (this.userTypeExit) {
1261
1264
  return this.props.delTab(this.props.tab.id)
1262
1265
  }
1263
- const key = `open${Date.now()}`
1264
- function closeMsg () {
1265
- notification.destroy(key)
1266
- }
1267
- this.socketCloseWarning = notification.warning({
1268
- key,
1269
- message: e('socketCloseTip'),
1270
- duration: 30,
1271
- description: (
1272
- <div className='pd2y'>
1273
- <Button
1274
- className='mg1r'
1275
- type='primary'
1276
- onClick={() => {
1277
- closeMsg()
1278
- this.props.delTab(this.props.tab.id)
1279
- }}
1280
- >
1281
- {e('close')}
1282
- </Button>
1283
- <Button
1284
- icon={<ReloadOutlined />}
1285
- onClick={() => {
1286
- closeMsg()
1287
- this.props.reloadTab(
1288
- this.props.tab
1289
- )
1290
- }}
1291
- >
1292
- {e('reload')}
1293
- </Button>
1294
- </div>
1295
- )
1296
- })
1266
+ const { autoReconnectTerminal } = this.props.config
1267
+ const isActive = this.isActiveTerminal()
1268
+ const isFocused = window.focused
1269
+ if (autoReconnectTerminal) {
1270
+ if (isActive && isFocused) {
1271
+ this.socketCloseWarning = showSocketCloseWarning({
1272
+ tabId: this.props.tab.id,
1273
+ tab: this.props.tab,
1274
+ autoReconnect: true,
1275
+ delTab: this.props.delTab,
1276
+ reloadTab: this.props.reloadTab
1277
+ })
1278
+ } else {
1279
+ this.scheduleAutoReconnect(3000)
1280
+ }
1281
+ } else {
1282
+ if (!isActive || !isFocused) {
1283
+ return false
1284
+ }
1285
+ this.socketCloseWarning = showSocketCloseWarning({
1286
+ tabId: this.props.tab.id,
1287
+ tab: this.props.tab,
1288
+ autoReconnect: false,
1289
+ delTab: this.props.delTab,
1290
+ reloadTab: this.props.reloadTab
1291
+ })
1292
+ }
1293
+ }
1294
+
1295
+ scheduleAutoReconnect = (delay = 3000) => {
1296
+ clearTimeout(this.timers.reconnectTimer)
1297
+ clearInterval(this.timers.reconnectCountdown)
1298
+ const seconds = Math.round(delay / 1000)
1299
+ this.setState({ reconnectCountdown: seconds })
1300
+ let remaining = seconds
1301
+ this.timers.reconnectCountdown = setInterval(() => {
1302
+ remaining -= 1
1303
+ if (remaining <= 0) {
1304
+ clearInterval(this.timers.reconnectCountdown)
1305
+ this.timers.reconnectCountdown = null
1306
+ }
1307
+ this.setState({ reconnectCountdown: remaining <= 0 ? null : remaining })
1308
+ }, 1000)
1309
+ this.timers.reconnectTimer = setTimeout(() => {
1310
+ clearInterval(this.timers.reconnectCountdown)
1311
+ this.timers.reconnectCountdown = null
1312
+ this.setState({ reconnectCountdown: null })
1313
+ if (this.onClose || !this.props.config.autoReconnectTerminal) {
1314
+ return
1315
+ }
1316
+ const reconnectCount = (this.props.tab.autoReConnect || 0) + 1
1317
+ this.props.reloadTab({ ...this.props.tab, autoReConnect: reconnectCount })
1318
+ }, delay)
1319
+ }
1320
+
1321
+ handleCancelAutoReconnect = () => {
1322
+ clearTimeout(this.timers.reconnectTimer)
1323
+ clearInterval(this.timers.reconnectCountdown)
1324
+ this.timers.reconnectTimer = null
1325
+ this.timers.reconnectCountdown = null
1326
+ this.setState({ reconnectCountdown: null })
1297
1327
  }
1298
1328
 
1299
1329
  batchInput = (cmd) => {
@@ -1395,7 +1425,7 @@ class Term extends Component {
1395
1425
  totalLines: this.state.totalLines,
1396
1426
  height
1397
1427
  }
1398
- const spinCls = loading ? 'loading-wrapper' : 'hide'
1428
+ const spin = loading ? <Spin className='loading-wrapper' spinning={loading} /> : null
1399
1429
  return (
1400
1430
  <Dropdown {...dropdownProps}>
1401
1431
  <div
@@ -1412,7 +1442,11 @@ class Term extends Component {
1412
1442
  <RemoteFloatControl
1413
1443
  isFullScreen={fullscreen}
1414
1444
  />
1415
- <Spin className={spinCls} spinning={loading} />
1445
+ <ReconnectOverlay
1446
+ countdown={this.state.reconnectCountdown}
1447
+ onCancel={this.handleCancelAutoReconnect}
1448
+ />
1449
+ {spin}
1416
1450
  </div>
1417
1451
  </Dropdown>
1418
1452
  )
@@ -141,3 +141,15 @@
141
141
  .paste-text
142
142
  max-height 200px
143
143
  overflow-y scroll
144
+
145
+ .terminal-reconnect-overlay
146
+ position absolute
147
+ left 0
148
+ top 0
149
+ width 100%
150
+ height 100%
151
+ display flex
152
+ align-items center
153
+ justify-content center
154
+ z-index 110
155
+
@@ -505,7 +505,8 @@ export default Store => {
505
505
  'sftpCreated',
506
506
  'sshSftpSplitView',
507
507
  'sshTunnelResults',
508
- 'displayRaw'
508
+ 'displayRaw',
509
+ 'autoReConnect'
509
510
  ]
510
511
  const { history } = store
511
512
  const index = history.findIndex(d => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.0.6",
3
+ "version": "3.0.18",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",