@electerm/electerm-react 2.8.16 → 2.10.26

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 (48) hide show
  1. package/client/common/constants.js +3 -3
  2. package/client/common/pre.js +1 -120
  3. package/client/components/ai/ai-config.jsx +26 -3
  4. package/client/components/ai/ai-history-item.jsx +46 -0
  5. package/client/components/ai/ai-history.jsx +104 -0
  6. package/client/components/bookmark-form/ai-bookmark-form.jsx +338 -0
  7. package/client/components/bookmark-form/bookmark-form.styl +1 -1
  8. package/client/components/bookmark-form/bookmark-schema.js +192 -0
  9. package/client/components/bookmark-form/common/ai-category-select.jsx +32 -0
  10. package/client/components/bookmark-form/common/category-select.jsx +2 -4
  11. package/client/components/bookmark-form/common/fields.jsx +0 -10
  12. package/client/components/bookmark-form/config/ftp.js +2 -0
  13. package/client/components/bookmark-form/config/rdp.js +0 -1
  14. package/client/components/bookmark-form/config/session-config.js +3 -1
  15. package/client/components/bookmark-form/config/spice.js +43 -0
  16. package/client/components/bookmark-form/config/vnc.js +1 -2
  17. package/client/components/bookmark-form/fix-bookmark-default.js +134 -0
  18. package/client/components/bookmark-form/index.jsx +74 -14
  19. package/client/components/common/notification.jsx +34 -2
  20. package/client/components/common/notification.styl +18 -2
  21. package/client/components/main/wrapper.styl +0 -7
  22. package/client/components/rdp/rdp-session.jsx +44 -11
  23. package/client/components/session/session.jsx +13 -3
  24. package/client/components/setting-panel/deep-link-control.jsx +3 -2
  25. package/client/components/setting-panel/keywords-transport.jsx +0 -1
  26. package/client/components/shortcuts/shortcut-editor.jsx +12 -36
  27. package/client/components/shortcuts/shortcut-handler.js +11 -5
  28. package/client/components/sidebar/index.jsx +11 -1
  29. package/client/components/spice/spice-session.jsx +296 -0
  30. package/client/components/spice/spice.styl +4 -0
  31. package/client/components/tabs/add-btn-menu.jsx +9 -2
  32. package/client/components/terminal/attach-addon-custom.js +20 -76
  33. package/client/components/terminal/terminal.jsx +34 -28
  34. package/client/components/terminal/transfer-client-base.js +232 -0
  35. package/client/components/terminal/trzsz-client.js +306 -0
  36. package/client/components/terminal/xterm-loader.js +109 -0
  37. package/client/components/terminal/zmodem-client.js +13 -166
  38. package/client/components/text-editor/simple-editor.jsx +1 -2
  39. package/client/components/vnc/vnc-session.jsx +1 -1
  40. package/client/entry/electerm.jsx +0 -2
  41. package/client/store/load-data.js +1 -1
  42. package/client/store/store.js +1 -1
  43. package/client/store/system-menu.js +10 -0
  44. package/package.json +1 -1
  45. package/client/common/trzsz.js +0 -46
  46. package/client/components/bookmark-form/common/wiki-alert.jsx +0 -9
  47. package/client/components/common/notification-with-details.jsx +0 -34
  48. package/client/components/terminal/fs.js +0 -59
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useEffect, useRef } from 'react'
2
- import { CloseOutlined } from '@ant-design/icons'
2
+ import { CloseOutlined, CopyOutlined } from '@ant-design/icons'
3
3
  import classnames from 'classnames'
4
4
  import generateId from '../../common/uid'
5
5
  import { messageIcons } from '../../common/icon-helpers.jsx'
6
+ import { copy } from '../../common/clipboard'
6
7
  import './notification.styl'
7
8
 
8
9
  const notifications = []
@@ -69,6 +70,19 @@ export function NotificationContainer () {
69
70
  )
70
71
  }
71
72
 
73
+ function getTextFromReactChildren (children) {
74
+ if (typeof children === 'string' || typeof children === 'number') {
75
+ return String(children)
76
+ }
77
+ if (React.isValidElement(children)) {
78
+ return getTextFromReactChildren(children.props.children)
79
+ }
80
+ if (Array.isArray(children)) {
81
+ return children.map(getTextFromReactChildren).join('\n')
82
+ }
83
+ return ''
84
+ }
85
+
72
86
  function NotificationItem ({ message, description, type, onClose, duration = 18.5 }) {
73
87
  const timeoutRef = useRef(null)
74
88
 
@@ -97,6 +111,12 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
97
111
  }
98
112
  }
99
113
 
114
+ const handleCopy = (text, e) => {
115
+ e.stopPropagation()
116
+ const textToCopy = getTextFromReactChildren(text)
117
+ copy(textToCopy)
118
+ }
119
+
100
120
  const className = classnames('notification', type)
101
121
 
102
122
  return (
@@ -109,8 +129,20 @@ function NotificationItem ({ message, description, type, onClose, duration = 18.
109
129
  <div className='notification-message'>
110
130
  <div className='notification-icon'>{messageIcons[type]}</div>
111
131
  <div className='notification-title' title={message}>{message}</div>
132
+ <CopyOutlined
133
+ className='notification-copy-icon'
134
+ onClick={(e) => handleCopy(message, e)}
135
+ />
112
136
  </div>
113
- {description && <div className='notification-description'>{description}</div>}
137
+ {description && (
138
+ <div className='notification-description'>
139
+ {description}
140
+ <CopyOutlined
141
+ className='notification-copy-icon'
142
+ onClick={(e) => handleCopy(description, e)}
143
+ />
144
+ </div>
145
+ )}
114
146
  </div>
115
147
  <CloseOutlined className='notification-close' onClick={onClose} />
116
148
  </div>
@@ -33,9 +33,10 @@
33
33
  top 10px
34
34
 
35
35
  .notification-description
36
- word-wrap break-all
36
+ word-wrap break-word
37
37
  max-height 200px
38
38
  overflow auto
39
+ padding 10px 0 0 0
39
40
 
40
41
  .notification-close
41
42
  position absolute
@@ -48,4 +49,19 @@
48
49
  cursor pointer
49
50
  padding 0
50
51
  &:hover
51
- color var(--text)
52
+ color var(--text)
53
+
54
+ .notification-copy-icon
55
+ cursor pointer
56
+ font-size 14px
57
+ position absolute
58
+ right 10px
59
+ top 45px
60
+ margin-left 8px
61
+ display none
62
+ .notification-message .notification-copy-icon
63
+ right 38px
64
+ top 14px
65
+ .notification-message:hover .notification-copy-icon,
66
+ .notification-description:hover .notification-copy-icon
67
+ display inline-block
@@ -1,10 +1,3 @@
1
- .common-err-desc
2
- &:hover
3
- text-overflow clip
4
- overflow-x hidden
5
- white-space pre
6
- max-height 300px
7
- overflow scroll
8
1
 
9
2
  .error-wrapper
10
3
  background var(--main)
@@ -6,14 +6,15 @@ import { handleErr } from '../../common/fetch'
6
6
  import {
7
7
  statusMap
8
8
  } from '../../common/constants'
9
- import {
10
- Spin,
11
- Select
12
- } from 'antd'
13
9
  import {
14
10
  ReloadOutlined,
15
11
  EditOutlined
16
12
  } from '@ant-design/icons'
13
+ import {
14
+ Spin,
15
+ Select,
16
+ Switch
17
+ } from 'antd'
17
18
  import * as ls from '../../common/safe-local-storage'
18
19
  import scanCode from './code-scan'
19
20
  import resolutions from './resolutions'
@@ -46,10 +47,13 @@ export default class RdpSession extends PureComponent {
46
47
  constructor (props) {
47
48
  const id = `rdp-reso-${props.tab.host}`
48
49
  const resObj = ls.getItemJSON(id, resolutions[1])
50
+ const scaleViewportId = `rdp-scale-view-${props.tab.host}`
51
+ const scaleViewport = ls.getItemJSON(scaleViewportId, false)
49
52
  super(props)
50
53
  this.canvasRef = createRef()
51
54
  this.state = {
52
55
  loading: false,
56
+ scaleViewport,
53
57
  ...resObj
54
58
  }
55
59
  this.session = null
@@ -76,7 +80,7 @@ export default class RdpSession extends PureComponent {
76
80
  }
77
81
  }
78
82
 
79
- runInitScript = () => {}
83
+ runInitScript = () => { }
80
84
 
81
85
  setStatus = status => {
82
86
  const id = this.props.tab?.id
@@ -267,7 +271,7 @@ export default class RdpSession extends PureComponent {
267
271
  const kind = e.kind ? e.kind() : 'Unknown'
268
272
  const bt = e.backtrace ? e.backtrace() : ''
269
273
  return `[${kindNames[kind] || kind}] ${bt}`
270
- } catch (_) {}
274
+ } catch (_) { }
271
275
  }
272
276
  return e?.message || e?.toString() || String(e)
273
277
  }
@@ -446,6 +450,14 @@ export default class RdpSession extends PureComponent {
446
450
  this.setState(res, this.handleReInit)
447
451
  }
448
452
 
453
+ handleScaleViewChange = (v) => {
454
+ const scaleViewportId = `rdp-scale-view-${this.props.tab.host}`
455
+ ls.setItemJSON(scaleViewportId, v)
456
+ this.setState({
457
+ scaleViewport: v
458
+ })
459
+ }
460
+
449
461
  renderHelp = () => {
450
462
  return null
451
463
  }
@@ -462,7 +474,7 @@ export default class RdpSession extends PureComponent {
462
474
  onSendCtrlAltDel: this.handleSendCtrlAltDel,
463
475
  screens: [], // RDP doesn't have multi-screen support like VNC
464
476
  currentScreen: null,
465
- onSelectScreen: () => {}, // No-op for RDP
477
+ onSelectScreen: () => { }, // No-op for RDP
466
478
  fixedPosition,
467
479
  showExitFullscreen,
468
480
  className
@@ -517,8 +529,17 @@ export default class RdpSession extends PureComponent {
517
529
  onChange: this.handleResChange,
518
530
  popupMatchSelectWidth: false
519
531
  }
532
+ const scaleProps = {
533
+ checked: this.state.scaleViewport,
534
+ onChange: this.handleScaleViewChange,
535
+ unCheckedChildren: window.translate('scaleViewport'),
536
+ checkedChildren: window.translate('scaleViewport'),
537
+ className: 'mg1l'
538
+ }
520
539
  return (
521
- <div className='pd1 fix session-v-info'>
540
+ <div
541
+ className='pd1 fix session-v-info'
542
+ >
522
543
  <div className='fleft'>
523
544
  <ReloadOutlined
524
545
  onClick={this.handleReInit}
@@ -547,6 +568,9 @@ export default class RdpSession extends PureComponent {
547
568
  />
548
569
  {this.renderInfo()}
549
570
  {this.renderHelp()}
571
+ <Switch
572
+ {...scaleProps}
573
+ />
550
574
  </div>
551
575
  <div className='fright'>
552
576
  {this.props.fullscreenIcon()}
@@ -582,25 +606,34 @@ export default class RdpSession extends PureComponent {
582
606
  height: h + 'px'
583
607
  }
584
608
  }
585
- const { width, height, loading } = this.state
609
+ const { width, height, loading, scaleViewport } = this.state
586
610
  const canvasProps = {
587
611
  width,
588
612
  height,
589
613
  tabIndex: 0
590
614
  }
615
+ if (scaleViewport) {
616
+ Object.assign(canvasProps, {
617
+ style: {
618
+ width: '100%',
619
+ objectFit: 'contain'
620
+ }
621
+ })
622
+ }
623
+ const cls = 'rdp-session-wrap session-v-wrap'
591
624
  const controlProps = this.getControlProps()
592
625
  return (
593
626
  <Spin spinning={loading}>
594
627
  <div
595
628
  {...rdpProps}
596
- className='rdp-session-wrap session-v-wrap'
629
+ className={cls}
597
630
  >
598
631
  {this.renderControl()}
599
- <RemoteFloatControl {...controlProps} />
600
632
  <canvas
601
633
  {...canvasProps}
602
634
  ref={this.canvasRef}
603
635
  />
636
+ <RemoteFloatControl {...controlProps} />
604
637
  </div>
605
638
  </Spin>
606
639
  )
@@ -8,6 +8,7 @@ import Sftp from '../sftp/sftp-entry'
8
8
  import RdpSession from '../rdp/rdp-session'
9
9
  import VncSession from '../vnc/vnc-session'
10
10
  import WebSession from '../web/web-session.jsx'
11
+ import SpiceSession from '../spice/spice-session'
11
12
  import {
12
13
  SearchOutlined,
13
14
  FullscreenOutlined,
@@ -29,7 +30,8 @@ import {
29
30
  terminalVncType,
30
31
  terminalWebType,
31
32
  terminalTelnetType,
32
- terminalFtpType
33
+ terminalFtpType,
34
+ terminalSpiceType
33
35
  } from '../../common/constants'
34
36
  import { SplitViewIcon } from '../icons/split-view'
35
37
  import { refs } from '../common/ref'
@@ -269,7 +271,7 @@ export default class SessionWrapper extends Component {
269
271
  />
270
272
  )
271
273
  }
272
- if (type === terminalRdpType || type === terminalVncType) {
274
+ if (type === terminalRdpType || type === terminalVncType || type === terminalSpiceType) {
273
275
  const rdpProps = {
274
276
  tab: this.props.tab,
275
277
  ...pick(this.props, [
@@ -299,6 +301,13 @@ export default class SessionWrapper extends Component {
299
301
  />
300
302
  )
301
303
  }
304
+ if (type === terminalSpiceType) {
305
+ return (
306
+ <SpiceSession
307
+ {...rdpProps}
308
+ />
309
+ )
310
+ }
302
311
 
303
312
  return (
304
313
  <RdpSession
@@ -373,7 +382,8 @@ export default class SessionWrapper extends Component {
373
382
  type === terminalVncType ||
374
383
  type === terminalWebType ||
375
384
  type === terminalTelnetType ||
376
- type === terminalFtpType
385
+ type === terminalFtpType ||
386
+ type === terminalSpiceType
377
387
  }
378
388
 
379
389
  calcSftpWidthHeight = () => {
@@ -65,12 +65,13 @@ export default function DeepLinkControl () {
65
65
  }
66
66
 
67
67
  const renderTooltipContent = () => {
68
- const protocols = ['ssh', 'telnet']
68
+ const protocols = ['ssh', 'telnet', 'rdp', 'vnc', 'serial', 'spice', 'electerm']
69
+ const tip = `Register electerm to handle protocol URLs (${protocols.join('://, ')})`
69
70
 
70
71
  return (
71
72
  <div>
72
73
  <div className='pd1b'>
73
- Register electerm to handle protocol URLs (ssh://, telnet://)
74
+ {tip}
74
75
  </div>
75
76
 
76
77
  {registrationStatus && (
@@ -27,7 +27,6 @@ export default class KeywordsTransport extends BookmarkTransport {
27
27
  const { store } = this.props
28
28
  const arr = store.config.keywords || []
29
29
  const txt = JSON.stringify(arr, null, 2)
30
- console.log(txt, 'txt')
31
30
  const stamp = time(undefined, 'YYYY-MM-DD-HH-mm-ss')
32
31
  download('electerm-' + this.name + '-' + stamp + '.json', txt)
33
32
  }
@@ -1,4 +1,4 @@
1
- import { PureComponent } from 'react'
1
+ import { PureComponent, createRef } from 'react'
2
2
  import {
3
3
  Button,
4
4
  Input
@@ -19,39 +19,27 @@ export default class ShortcutEdit extends PureComponent {
19
19
  data: null
20
20
  }
21
21
 
22
+ containerRef = createRef()
23
+
24
+ componentWillUnmount () {
25
+ this.removeEventListener()
26
+ }
27
+
22
28
  addEventListener = () => {
23
- const elem = document.querySelector('.ant-drawer')
24
- elem?.addEventListener('click', this.handleClickOuter)
29
+ document.addEventListener('click', this.handleClickOuter, true)
25
30
  document.addEventListener('keydown', this.handleKeyDown)
26
31
  document.addEventListener('mousewheel', this.handleKeyDown)
27
32
  }
28
33
 
29
34
  removeEventListener = () => {
30
- const elem = document.querySelector('.ant-drawer')
31
- elem?.removeEventListener('click', this.handleClickOuter)
35
+ document.removeEventListener('click', this.handleClickOuter, true)
32
36
  document.removeEventListener('keydown', this.handleKeyDown)
33
37
  document.removeEventListener('mousewheel', this.handleKeyDown)
34
38
  }
35
39
 
36
- isInsideElement = (event) => {
37
- const { target } = event
38
- const cls = this.getCls()
39
- if (!target || !target.classList) {
40
- return false
41
- } else if (target.classList.contains(cls)) {
42
- return true
43
- } else {
44
- const parent = target.parentElement
45
- if (parent !== null) {
46
- return this.isInsideElement({ target: parent })
47
- } else {
48
- return false
49
- }
50
- }
51
- }
52
-
53
40
  handleClickOuter = (e) => {
54
- if (!this.isInsideElement(e)) {
41
+ const container = this.containerRef.current
42
+ if (container && !container.contains(e.target)) {
55
43
  this.handleCancel()
56
44
  }
57
45
  }
@@ -78,11 +66,6 @@ export default class ShortcutEdit extends PureComponent {
78
66
  this.handleCancel()
79
67
  }
80
68
 
81
- getCls = () => {
82
- const { index } = this.props.data
83
- return 'shortcut-control-' + index
84
- }
85
-
86
69
  warnCtrolKey = throttle(() => {
87
70
  message.info(
88
71
  'Must have one of Ctrl or Shift or Alt or Meta key',
@@ -131,13 +114,6 @@ export default class ShortcutEdit extends PureComponent {
131
114
  })
132
115
  }
133
116
 
134
- handleClickOutside = () => {
135
- this.removeEventListener()
136
- this.setState({
137
- editMode: false
138
- })
139
- }
140
-
141
117
  renderStatic () {
142
118
  const {
143
119
  shortcut
@@ -199,7 +175,7 @@ export default class ShortcutEdit extends PureComponent {
199
175
  return this.renderStatic()
200
176
  }
201
177
  return (
202
- <div className={this.getCls()}>
178
+ <div ref={this.containerRef}>
203
179
  <Input
204
180
  suffix={this.renderAfter()}
205
181
  value={shortcut}
@@ -175,12 +175,18 @@ export function shortcutExtend (Cls) {
175
175
  type === 'keydown' &&
176
176
  !altKey &&
177
177
  !shiftKey &&
178
- ctrlKey &&
179
- this.zmodemClient &&
180
- this.zmodemClient.isActive
178
+ ctrlKey
181
179
  ) {
182
- this.zmodemClient.cancel()
183
- return false
180
+ // Cancel zmodem transfer if active
181
+ if (this.zmodemClient && this.zmodemClient.isActive) {
182
+ this.zmodemClient.cancel()
183
+ return false
184
+ }
185
+ // Cancel trzsz transfer if active
186
+ if (this.trzszClient && this.trzszClient.isActive) {
187
+ this.trzszClient.cancel()
188
+ return false
189
+ }
184
190
  }
185
191
 
186
192
  let codeName
@@ -7,7 +7,8 @@ import {
7
7
  SettingOutlined,
8
8
  UpCircleOutlined,
9
9
  BarsOutlined,
10
- AppstoreOutlined
10
+ AppstoreOutlined,
11
+ RobotOutlined
11
12
  } from '@ant-design/icons'
12
13
  import { Tooltip } from 'antd'
13
14
  import SideBarPanel from './sidebar-panel'
@@ -86,6 +87,7 @@ export default function Sidebar (props) {
86
87
 
87
88
  const {
88
89
  onNewSsh,
90
+ onNewSshAI,
89
91
  openSetting,
90
92
  openAbout,
91
93
  openSettingSync,
@@ -142,6 +144,14 @@ export default function Sidebar (props) {
142
144
  onClick={onNewSsh}
143
145
  />
144
146
  </SideIcon>
147
+ <SideIcon
148
+ title={e('createBookmarkByAI')}
149
+ >
150
+ <RobotOutlined
151
+ className='font20 iblock control-icon'
152
+ onClick={onNewSshAI}
153
+ />
154
+ </SideIcon>
145
155
  <SideIcon
146
156
  title={e(settingMap.bookmarks)}
147
157
  active={bookmarksActive}