@electerm/electerm-react 3.8.15 → 3.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/client/common/pre.js +0 -1
  2. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +0 -1
  3. package/client/components/bookmark-form/config/rdp.js +4 -2
  4. package/client/components/icons/heartbeat.jsx +23 -0
  5. package/client/components/main/main.jsx +3 -1
  6. package/client/components/session/session.jsx +3 -3
  7. package/client/components/setting-sync/setting-sync-form.jsx +9 -1
  8. package/client/components/setting-sync/setting-sync.jsx +2 -1
  9. package/client/components/sftp/list-table-ui.jsx +33 -54
  10. package/client/components/sftp/paged-list.jsx +44 -44
  11. package/client/components/sftp/sftp-entry.jsx +5 -4
  12. package/client/components/sidebar/info-modal.jsx +8 -2
  13. package/client/components/tabs/tab.jsx +38 -19
  14. package/client/components/tabs/tabs.styl +8 -17
  15. package/client/components/terminal/attach-addon-custom.js +7 -3
  16. package/client/components/terminal/reconnect-overlay.jsx +2 -15
  17. package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
  18. package/client/components/terminal/terminal-error-handle.jsx +43 -0
  19. package/client/components/terminal/terminal.jsx +40 -38
  20. package/client/components/terminal/terminal.styl +12 -7
  21. package/client/components/terminal/unix-timestamp-tooltip.jsx +85 -0
  22. package/client/components/text-editor/edit-with-custom-editor.jsx +22 -3
  23. package/client/components/text-editor/text-editor.jsx +21 -0
  24. package/client/components/tree-list/bookmark-toolbar.jsx +5 -5
  25. package/client/components/tree-list/tree-list-item.jsx +6 -12
  26. package/client/components/tree-list/tree-list-row.jsx +5 -0
  27. package/client/components/tree-list/tree-list-rows.js +3 -1
  28. package/client/components/widgets/widget-control.jsx +1 -0
  29. package/client/store/common.js +1 -1
  30. package/client/store/load-data.js +2 -2
  31. package/client/store/mcp-handler.js +83 -10
  32. package/client/store/sync.js +3 -2
  33. package/client/store/tab.js +34 -0
  34. package/package.json +1 -1
  35. package/client/components/terminal/socket-close-warning.jsx +0 -94
@@ -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,29 @@ 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
+ const prevList = prevProps.fileList
32
+ const nextList = this.props.fileList
33
+ const contentChanged = prevList.length !== nextList.length ||
34
+ prevList.some((f, i) => f.id !== nextList[i].id)
35
+ if (contentChanged) {
36
+ if (this.containerRef.current) {
37
+ this.containerRef.current.scrollTop = 0
38
+ }
39
+ this.setState({ scrollTop: 0 })
40
+ }
41
+ }
42
+
43
+ onScroll = (e) => {
44
+ this.setState({ scrollTop: e.target.scrollTop })
23
45
  }
24
46
 
25
47
  initFromProps = (pps = this.getPropsDefault()) => {
@@ -36,9 +58,7 @@ export default class FileListTable extends Component {
36
58
  }
37
59
  })
38
60
  return {
39
- pageSize: 100,
40
- properties,
41
- page: 1
61
+ properties
42
62
  }
43
63
  }
44
64
 
@@ -171,40 +191,6 @@ export default class FileListTable extends Component {
171
191
  'left'
172
192
  ]
173
193
 
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
194
  // reset
209
195
  resetWidth = () => {
210
196
  this.setState(this.initFromProps())
@@ -293,24 +279,19 @@ export default class FileListTable extends Component {
293
279
 
294
280
  render () {
295
281
  const { fileList, height, type } = this.props
296
- // const tableHeaderHeight = 30
297
- // const sh = sshSftpSplitView ? 0 : 32
282
+ const containerHeight = height - 42 - 30 - 32 - 90
298
283
  const props = {
284
+ ref: this.containerRef,
299
285
  className: 'sftp-table-content overscroll-y relative',
300
286
  style: {
301
- height: height - 42 - 30 - 32 - 90
287
+ height: containerHeight
302
288
  },
303
289
  draggable: false,
290
+ onScroll: this.onScroll,
304
291
  onClick: this.handleClick,
305
292
  onDoubleClick: this.handleDoubleClick
306
293
  }
307
- const hasPager = this.hasPager()
308
- const cls = classnames(
309
- 'sftp-table relative',
310
- {
311
- 'sftp-has-pager': hasPager
312
- }
313
- )
294
+ const cls = classnames('sftp-table relative')
314
295
  const ddProps = {
315
296
  menu: {
316
297
  items: this.renderContextMenuFile(),
@@ -338,13 +319,11 @@ export default class FileListTable extends Component {
338
319
  <PagedList
339
320
  list={fileList}
340
321
  renderItem={this.renderItem}
341
- hasPager={hasPager}
342
- page={this.state.page}
343
- pageSize={this.state.pageSize}
322
+ scrollTop={this.state.scrollTop}
323
+ containerHeight={containerHeight}
344
324
  />
345
325
  </div>
346
326
  </Dropdown>
347
- {hasPager && this.renderPager()}
348
327
  </div>
349
328
  )
350
329
  }
@@ -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'>
@@ -8,7 +8,8 @@ import { refsTabs } from '../common/ref'
8
8
  import {
9
9
  CloseOutlined,
10
10
  Loading3QuartersOutlined,
11
- BorderlessTableOutlined
11
+ BorderlessTableOutlined,
12
+ LockOutlined
12
13
  } from '@ant-design/icons'
13
14
  import {
14
15
  Tooltip,
@@ -33,7 +34,7 @@ class Tab extends Component {
33
34
  constructor (props) {
34
35
  super(props)
35
36
  this.state = {
36
- terminalOnData: false
37
+ terminalOnData: ''
37
38
  }
38
39
  this.id = 'tab-' + this.props.tab.id
39
40
  refsTabs.add(this.id, this)
@@ -48,19 +49,38 @@ class Tab extends Component {
48
49
  }
49
50
 
50
51
  notifyOnData = () => {
52
+ if (this.state.terminalOnData === 'password') {
53
+ return
54
+ }
51
55
  if (this.timer) {
52
56
  clearTimeout(this.timer)
53
57
  this.timer = null
54
58
  }
55
59
  this.setState({
56
- terminalOnData: true
60
+ terminalOnData: 'feed'
57
61
  })
58
62
  this.timer = setTimeout(this.clearTerminalOnData, 4000)
59
63
  }
60
64
 
61
65
  clearTerminalOnData = () => {
62
66
  this.setState({
63
- terminalOnData: false
67
+ terminalOnData: ''
68
+ })
69
+ }
70
+
71
+ notifyPasswordPrompt = () => {
72
+ if (this.timer) {
73
+ clearTimeout(this.timer)
74
+ this.timer = null
75
+ }
76
+ this.setState({
77
+ terminalOnData: 'password'
78
+ })
79
+ }
80
+
81
+ clearPasswordPrompt = () => {
82
+ this.setState({
83
+ terminalOnData: ''
64
84
  })
65
85
  }
66
86
 
@@ -424,13 +444,7 @@ class Tab extends Component {
424
444
  {
425
445
  'tab-last': isLast
426
446
  },
427
- status,
428
- {
429
- 'is-terminal-active': terminalOnData
430
- },
431
- {
432
- 'is-transporting': isTransporting
433
- }
447
+ status
434
448
  )
435
449
  const title = createName(tab)
436
450
  let tooltipTitle = title
@@ -477,15 +491,19 @@ class Tab extends Component {
477
491
  >
478
492
  <Dropdown {...dropdownProps}>
479
493
  <div
480
- className='tab-title elli pd1x'
494
+ className='tab-title elli'
481
495
  onClick={this.handleClick}
482
496
  onDoubleClick={this.handleDup}
483
497
  >
484
- <Loading3QuartersOutlined
485
- className='pointer tab-reload mg1r'
486
- onClick={this.handleReloadTab}
487
- title={e('reload')}
488
- />
498
+ {
499
+ status === 'error' && (
500
+ <Loading3QuartersOutlined
501
+ className='pointer tab-reload mg1r'
502
+ onClick={this.handleReloadTab}
503
+ title={e('reload')}
504
+ />
505
+ )
506
+ }
489
507
  <span className='tab-title'>
490
508
  <span className='iblock mg1r tab-count' style={styleTag}>{tabCount}</span>
491
509
  <span className='mg1r'>{title}</span>
@@ -493,8 +511,9 @@ class Tab extends Component {
493
511
  </div>
494
512
  </Dropdown>
495
513
  <div className={'tab-status ' + status} />
496
- <div className='tab-traffic' />
497
- <BorderlessTableOutlined className='tab-terminal-feed' />
514
+ {isTransporting && <div className='tab-traffic' />}
515
+ {terminalOnData === 'feed' && <BorderlessTableOutlined className='tab-terminal-feed' />}
516
+ {terminalOnData === 'password' && <LockOutlined className='tab-terminal-feed password' />}
498
517
  {
499
518
  this.renderCloseIcon()
500
519
  }
@@ -36,6 +36,7 @@
36
36
  vertical-align middle
37
37
  cursor pointer
38
38
  position relative
39
+ padding 0 14px
39
40
  min-width 100px
40
41
  max-width 200px
41
42
  line-height 36px
@@ -46,7 +47,6 @@
46
47
  color var(--text-dark)
47
48
  &.tab-last
48
49
  margin-right 5px
49
- .tab-reload
50
50
  .tab-close
51
51
  display none
52
52
  &.active
@@ -68,10 +68,6 @@
68
68
  width 1px
69
69
  border 1px dashed var(--text-dark)
70
70
  height 36px
71
- &.error
72
- .tab-reload
73
- display inline-block
74
- color var(--text-light)
75
71
  @keyframes blink
76
72
  0%
77
73
  background-color #e0e0e0
@@ -96,30 +92,25 @@
96
92
  background-color var(--error)
97
93
  &.processing
98
94
  background-color var(--primary)
99
- .is-transporting .tab-traffic
100
- display block
101
- animation blink 2s infinite
102
- /* Remove opacity animation, use background-color */
103
95
  .tab-traffic
104
- display none
105
96
  left 10px
106
97
  width 5px
107
98
  border-radius 0
108
99
  background-color var(--success)
109
- .is-terminal-active .tab-terminal-feed
110
- display block
111
100
  animation blink 2s infinite
112
- background-color transparent !important
113
- /* Remove opacity animation, use background-color */
114
101
  .tab-terminal-feed
115
- display none
116
- left 20px
102
+ width 12px
103
+ height 12px
117
104
  border-radius 0
118
105
  color var(--success)
119
- font-size 8px
106
+ font-size 12px
120
107
  left 2px
121
108
  top 24px
122
109
  background none
110
+ background-color transparent !important
111
+ animation blink 2s infinite
112
+ &.password
113
+ color var(--warn)
123
114
  .tab-close
124
115
  position absolute
125
116
  right 5px
@@ -223,6 +223,7 @@ export default class AttachAddonCustom {
223
223
  }
224
224
 
225
225
  if (typeof data === 'string') {
226
+ term?.parent?.notifyOnData()
226
227
  return term.write(data)
227
228
  }
228
229
  data = new Uint8Array(data)
@@ -242,13 +243,16 @@ export default class AttachAddonCustom {
242
243
  sendToServer = (data) => {
243
244
  this._lastInputTime = Date.now()
244
245
  // Start echo detection when password prompt is suspected
245
- if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
246
+ if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n' && data !== '\x03') {
246
247
  this._pendingEchoCheck = { char: data, time: Date.now() }
247
248
  clearTimeout(this._echoCheckTimer)
248
249
  this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
249
250
  }
250
- // Reset password state on Enter
251
- if (data === '\r' || data === '\n') {
251
+ // Reset password state on Enter or Ctrl+C
252
+ if (data === '\r' || data === '\n' || data === '\x03') {
253
+ if (this._passwordPromptDetected) {
254
+ this.term?.parent?.onPasswordPromptCancelled?.()
255
+ }
252
256
  this._passwordPromptDetected = false
253
257
  this._lastOutputLine = ''
254
258
  this._pendingEchoCheck = null