@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.
@@ -9,7 +9,6 @@ const {
9
9
 
10
10
  const props = runSync('getConstants')
11
11
 
12
- props.env = JSON.parse(props.env)
13
12
  props.versions = JSON.parse(props.versions)
14
13
 
15
14
  window.pre = {
@@ -49,7 +49,6 @@ export default class BookmarkFromHistoryModal extends React.PureComponent {
49
49
  ...tab,
50
50
  id: generate()
51
51
  }
52
- console.log(r)
53
52
  delete r.parentId
54
53
  delete r.category
55
54
  return r
@@ -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.addTab(parsed))
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
- <HeartOutlined {...iconProps} />
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, Pagination } from 'antd'
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 = this.initFromProps()
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
- pageSize: 100,
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
- // const tableHeaderHeight = 30
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: height - 42 - 30 - 32 - 90
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 hasPager = this.hasPager()
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
- hasPager={hasPager}
342
- page={this.state.page}
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
- * file list module to limit files rendered to increase performance
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
- export default class ScrollFiles extends Component {
9
- state = {
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
- onChange = page => {
15
- this.setState({
16
- page
17
- })
18
- }
22
+ export default class VirtualList extends Component {
23
+ rootRef = createRef()
19
24
 
20
- renderList () {
21
- const page = this.props.page ?? this.state.page
22
- const pageSize = this.props.pageSize ?? this.state.pageSize
23
- const start = (page - 1) * pageSize
24
- const end = start + pageSize
25
- const {
26
- list, hasPager
27
- } = this.props
28
- const arr = hasPager
29
- ? list.slice(start, end)
30
- : list
31
- return arr.map((item, index) => this.props.renderItem(item, index))
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 className='pd1b pager-wrap'>
46
- <Pagination
47
- {...props}
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.env[envName]
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 { env, versions } = window.pre
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
- ...env
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, onCancel }) {
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
- <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>
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.setState({
962
- hasSelection: this.term.hasSelection()
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
- handleErr({ message: text })
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
- if (isActive && isFocused) {
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
- left 0
157
- top 0
158
- width 100%
159
- height 100%
160
- display flex
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
- <div className='fix'>
91
- <div className='fleft'>
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 className='fright'>
125
+ <div>
126
126
  <Dropdown {...ddProps}>
127
127
  <MenuOutlined />
128
128
  </Dropdown>
129
129
  </div>
130
- </div>
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
  }
@@ -298,7 +298,7 @@ export default Store => {
298
298
  }
299
299
 
300
300
  Store.prototype.aiConfigMissing = function () {
301
- return aiConfigsArr.slice(0, -1).some(k => !window.store.config[k])
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.addTab(conf)
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.addTab(pending)
232
+ window.store.ipcOpenTab(pending)
233
233
  }
234
234
  }
235
235
  Store.prototype.parseQuickConnect = function (url) {
@@ -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
- return [serverUrl, username, password].join('####')
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
  }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.8.15",
3
+ "version": "3.9.5",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -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
- }