@electerm/electerm-react 3.15.175 → 3.15.178

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.
@@ -7,6 +7,11 @@
7
7
  .main-footer
8
8
  .session-v-info
9
9
  display none
10
+ // Mobile tabs carry the extra .mobile-tabs class, and mobile.styl uses
11
+ // .is-mobile .tabs.mobile-tabs (specificity 0,3,0) which overrides the
12
+ // .fullscreen .tabs rule above (0,2,0). This ensures they stay hidden.
13
+ .tabs.mobile-tabs
14
+ display none
10
15
  // Hide all sessions first
11
16
  .session-wrap
12
17
  display none
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Session control bar — pane tabs and action icons.
3
+ * On mobile a menu icon is shown; clicking it toggles the control icons.
4
+ */
5
+ import {
6
+ SearchOutlined,
7
+ FullscreenOutlined,
8
+ PaperClipOutlined,
9
+ CloseOutlined,
10
+ ApartmentOutlined,
11
+ MoreOutlined,
12
+ ColumnWidthOutlined
13
+ } from '@ant-design/icons'
14
+ import { Tooltip, Popover } from 'antd'
15
+ import classnames from 'classnames'
16
+ import {
17
+ paneMap,
18
+ connectionMap
19
+ } from '../../common/constants'
20
+ import { SplitViewIcon } from '../icons/split-view'
21
+ import { HeartbeatIcon } from '../icons/heartbeat'
22
+ import './session-control.styl'
23
+
24
+ const e = window.translate
25
+
26
+ export default function SessionControl (props) {
27
+ const {
28
+ tab,
29
+ isMobile,
30
+ isDisabled,
31
+ isSshDisabled,
32
+ isNotTerminalType,
33
+ canSplitView,
34
+ sftpPathFollowSsh,
35
+ keepaliveEnabled,
36
+ broadcastInput,
37
+ wrapDisabled,
38
+ delKeyPressed,
39
+ hideDelKeyTip,
40
+ onChangePane,
41
+ toggleCheckSftpPathFollowSsh,
42
+ onSshSftpSplitView,
43
+ toggleKeepalive,
44
+ toggleBroadcastInput,
45
+ toggleWrap,
46
+ onFullscreen,
47
+ onOpenSearch,
48
+ onDismissDelKeyTip
49
+ } = props
50
+
51
+ if (isNotTerminalType) {
52
+ return null
53
+ }
54
+
55
+ // ---- sub-renderers ----
56
+
57
+ function renderPaneControl () {
58
+ if (isDisabled) {
59
+ return null
60
+ }
61
+ const { sshSftpSplitView, pane } = tab
62
+ if (sshSftpSplitView && canSplitView) {
63
+ return null
64
+ }
65
+ const termType = tab?.type
66
+ const isSsh = tab.authType
67
+ const isLocal = !isSsh && (termType === connectionMap.local || !termType)
68
+ const types = [
69
+ paneMap.terminal,
70
+ paneMap.fileManager
71
+ ]
72
+ const controls = [
73
+ isSsh ? paneMap.ssh : paneMap.terminal
74
+ ]
75
+ if (isSsh || isLocal) {
76
+ controls.push(isSsh ? paneMap.sftp : paneMap.fileManager)
77
+ }
78
+ return (
79
+ <div className='term-sftp-tabs fleft'>
80
+ {
81
+ controls.map((type, i) => {
82
+ const cls = classnames(
83
+ 'type-tab',
84
+ type,
85
+ {
86
+ active: types[i] === pane
87
+ }
88
+ )
89
+ return (
90
+ <span
91
+ className={cls}
92
+ key={type + '_' + i}
93
+ onClick={() => onChangePane(types[i])}
94
+ >
95
+ <span className='type-tab-txt'>
96
+ {e(type)}
97
+ <span className='type-tab-line' />
98
+ </span>
99
+ </span>
100
+ )
101
+ })
102
+ }
103
+ </div>
104
+ )
105
+ }
106
+
107
+ function renderDelTip (isS) {
108
+ if (!isS || hideDelKeyTip || !delKeyPressed) {
109
+ return null
110
+ }
111
+ return (
112
+ <div className='type-tab'>
113
+ <span className='mg1r'>Try <b>Shift + Backspace</b>?</span>
114
+ <CloseOutlined
115
+ onClick={onDismissDelKeyTip}
116
+ className='pointer'
117
+ />
118
+ </div>
119
+ )
120
+ }
121
+
122
+ function renderSftpPathFollowControl () {
123
+ if (isDisabled) {
124
+ return null
125
+ }
126
+ const { pane, enableSsh, sshSftpSplitView } = tab
127
+ const termType = tab?.type
128
+ const isSsh = tab.authType
129
+ const isLocal = !isSsh && (termType === connectionMap.local || !termType)
130
+ const checkTxt = e('sftpPathFollowSsh')
131
+ const checkProps = {
132
+ onClick: toggleCheckSftpPathFollowSsh,
133
+ className: classnames(
134
+ 'sftp-follow-ssh-icon sess-icon pointer',
135
+ {
136
+ active: sftpPathFollowSsh
137
+ }
138
+ )
139
+ }
140
+ const isS = pane === paneMap.terminal ||
141
+ sshSftpSplitView
142
+ return (
143
+ <>
144
+ {
145
+ (isSsh && enableSsh) || isLocal
146
+ ? (
147
+ <Tooltip title={checkTxt}>
148
+ <span {...checkProps}>
149
+ <PaperClipOutlined />
150
+ </span>
151
+ </Tooltip>
152
+ )
153
+ : null
154
+ }
155
+ {renderDelTip(isS)}
156
+ </>
157
+ )
158
+ }
159
+
160
+ function renderSplitToggle () {
161
+ if (isMobile) {
162
+ return null
163
+ }
164
+ if (!canSplitView || isNotTerminalType) {
165
+ return null
166
+ }
167
+ const title = e('sshSftpSplitView')
168
+ const { sshSftpSplitView } = tab
169
+ const cls = classnames(
170
+ 'pointer sess-icon split-view-toggle',
171
+ {
172
+ active: sshSftpSplitView
173
+ }
174
+ )
175
+ return (
176
+ <Tooltip title={title} placement='bottomLeft'>
177
+ <span
178
+ className={cls}
179
+ onClick={onSshSftpSplitView}
180
+ >
181
+ <SplitViewIcon />
182
+ </span>
183
+ </Tooltip>
184
+ )
185
+ }
186
+
187
+ function renderKeepaliveIcon () {
188
+ if (isSshDisabled) {
189
+ return null
190
+ }
191
+ const title = e('keepalive')
192
+ const iconProps = {
193
+ className: classnames('sess-icon pointer keepalive-icon', {
194
+ active: keepaliveEnabled
195
+ }),
196
+ onClick: toggleKeepalive
197
+ }
198
+ return (
199
+ <Tooltip title={title}>
200
+ <HeartbeatIcon {...iconProps} />
201
+ </Tooltip>
202
+ )
203
+ }
204
+
205
+ function renderBroadcastIcon () {
206
+ if (isSshDisabled) {
207
+ return null
208
+ }
209
+ const title = e('broadcastInput')
210
+ const iconProps = {
211
+ className: classnames('sess-icon pointer broadcast-icon', {
212
+ active: broadcastInput
213
+ }),
214
+ onClick: toggleBroadcastInput
215
+ }
216
+ return (
217
+ <Tooltip title={title}>
218
+ <ApartmentOutlined {...iconProps} />
219
+ </Tooltip>
220
+ )
221
+ }
222
+
223
+ function renderWrapIcon () {
224
+ const title = e(wrapDisabled ? 'enableWrap' : 'disableWrap')
225
+ const iconProps = {
226
+ className: classnames('sess-icon pointer wrap-toggle-icon', {
227
+ active: wrapDisabled
228
+ }),
229
+ onClick: toggleWrap
230
+ }
231
+ return (
232
+ <Tooltip title={title}>
233
+ <ColumnWidthOutlined {...iconProps} />
234
+ </Tooltip>
235
+ )
236
+ }
237
+
238
+ function renderSearchIcon () {
239
+ const title = e('search')
240
+ return (
241
+ <Tooltip title={title} placement='bottomLeft'>
242
+ <SearchOutlined
243
+ className='mg1r icon-info iblock pointer spliter'
244
+ onClick={onOpenSearch}
245
+ />
246
+ </Tooltip>
247
+ )
248
+ }
249
+
250
+ function renderFullscreenIcon () {
251
+ const title = e('fullscreen')
252
+ return (
253
+ <Tooltip title={title} placement='bottomLeft'>
254
+ <FullscreenOutlined
255
+ className='mg1r icon-info iblock pointer spliter fullscreen-control-icon'
256
+ onClick={onFullscreen}
257
+ />
258
+ </Tooltip>
259
+ )
260
+ }
261
+
262
+ function renderTermControls () {
263
+ const { pane } = tab
264
+ if (pane !== paneMap.terminal) {
265
+ return null
266
+ }
267
+ return (
268
+ <div className='fright term-controls'>
269
+ {renderFullscreenIcon()}
270
+ {renderSearchIcon()}
271
+ </div>
272
+ )
273
+ }
274
+
275
+ // ---- mobile ----
276
+
277
+ if (isMobile) {
278
+ const extraIcons = (
279
+ <div className='mobile-control-icons'>
280
+ {renderSftpPathFollowControl()}
281
+ {renderKeepaliveIcon()}
282
+ {renderBroadcastIcon()}
283
+ {renderWrapIcon()}
284
+ {renderTermControls()}
285
+ </div>
286
+ )
287
+ return (
288
+ <div className='terminal-control mobile-session-control'>
289
+ {renderPaneControl()}
290
+ <Popover
291
+ content={extraIcons}
292
+ trigger='click'
293
+ placement='bottomRight'
294
+ >
295
+ <MoreOutlined className='mobile-control-toggle pointer' />
296
+ </Popover>
297
+ </div>
298
+ )
299
+ }
300
+
301
+ // ---- desktop ----
302
+ return (
303
+ <div className='terminal-control fix'>
304
+ {renderPaneControl()}
305
+ {renderSftpPathFollowControl()}
306
+ {renderSplitToggle()}
307
+ {renderKeepaliveIcon()}
308
+ {renderBroadcastIcon()}
309
+ {renderWrapIcon()}
310
+ {renderTermControls()}
311
+ </div>
312
+ )
313
+ }
@@ -0,0 +1,38 @@
1
+ // Mobile session control: pane tabs always visible,
2
+ // other icons shown in a floating Popover overlay
3
+ .mobile-session-control
4
+ background var(--main)
5
+ line-height 32px
6
+ padding 0 10px
7
+ display flex
8
+ align-items center
9
+ // pane tabs take available space, menu icon sits at the end
10
+ .term-sftp-tabs
11
+ float none
12
+ display flex
13
+ align-items center
14
+ flex 1
15
+ min-width 0
16
+ .mobile-control-toggle
17
+ font-size 16px
18
+ line-height 32px
19
+ color var(--text)
20
+ flex-shrink 0
21
+ &:hover
22
+ color var(--text-light)
23
+
24
+ // Popover content — icons laid out in a row, wrapping as needed
25
+ .mobile-control-icons
26
+ display flex
27
+ flex-wrap wrap
28
+ align-items center
29
+ gap 4px 8px
30
+ max-width 280px
31
+ line-height 30px
32
+ .term-controls
33
+ float none
34
+ display flex
35
+ align-items center
36
+ margin-left 0
37
+ .sess-icon
38
+ margin-right 0
@@ -10,11 +10,7 @@ import VncSession from '../vnc/vnc-session'
10
10
  import WebSession from '../web/web-session.jsx'
11
11
  import SpiceSession from '../spice/spice-session'
12
12
  import {
13
- SearchOutlined,
14
- FullscreenOutlined,
15
- PaperClipOutlined,
16
- CloseOutlined,
17
- ApartmentOutlined
13
+ FullscreenOutlined
18
14
  } from '@ant-design/icons'
19
15
  import {
20
16
  Tooltip,
@@ -25,7 +21,6 @@ import copy from 'json-deep-copy'
25
21
  import classnames from 'classnames'
26
22
  import {
27
23
  paneMap,
28
- connectionMap,
29
24
  terminalRdpType,
30
25
  terminalVncType,
31
26
  terminalWebType,
@@ -33,10 +28,9 @@ import {
33
28
  terminalFtpType,
34
29
  terminalSpiceType
35
30
  } from '../../common/constants'
36
- import { SplitViewIcon } from '../icons/split-view'
37
31
  import { refs } from '../common/ref'
38
32
  import sanitizeFilename from '../../common/sanitize-filename.js'
39
- import { HeartbeatIcon } from '../icons/heartbeat'
33
+ import SessionControl from './session-control'
40
34
  import './session.styl'
41
35
 
42
36
  const e = window.translate
@@ -54,7 +48,8 @@ export default class SessionWrapper extends Component {
54
48
  sessionOptions: null,
55
49
  delKeyPressed: false,
56
50
  broadcastInput: false,
57
- keepaliveEnabled: false
51
+ keepaliveEnabled: false,
52
+ wrapDisabled: false
58
53
  }
59
54
  if (props.tab.sshSftpSplitView === undefined) {
60
55
  props.tab.sshSftpSplitView = !!props.config.sshSftpSplitView
@@ -499,16 +494,15 @@ export default class SessionWrapper extends Component {
499
494
  refs.get('term-' + this.props.tab.id)?.toggleSearch()
500
495
  }
501
496
 
502
- renderSearchIcon = () => {
503
- const title = e('search')
504
- return (
505
- <Tooltip title={title} placement='bottomLeft'>
506
- <SearchOutlined
507
- className='mg1r icon-info iblock pointer spliter'
508
- onClick={this.handleOpenSearch}
509
- />
510
- </Tooltip>
511
- )
497
+ toggleWrap = () => {
498
+ const termRef = refs.get('term-' + this.props.tab.id)
499
+ if (!termRef?.term) {
500
+ return
501
+ }
502
+ const next = !this.state.wrapDisabled
503
+ // DECAWM escape sequence: ?7l = disable wrap, ?7h = enable wrap
504
+ termRef.term.write(next ? '\x1b[?7l' : '\x1b[?7h')
505
+ this.setState({ wrapDisabled: next })
512
506
  }
513
507
 
514
508
  fullscreenIcon = () => {
@@ -523,237 +517,11 @@ export default class SessionWrapper extends Component {
523
517
  )
524
518
  }
525
519
 
526
- renderDelTip = (isSsh) => {
527
- if (!isSsh || this.props.hideDelKeyTip || !this.state.delKeyPressed) {
528
- return null
529
- }
530
- return (
531
- <div className='type-tab'>
532
- <span className='mg1r'>Try <b>Shift + Backspace</b>?</span>
533
- <CloseOutlined
534
- onClick={this.handleDismissDelKeyTip}
535
- className='pointer'
536
- />
537
- </div>
538
- )
539
- }
540
-
541
- renderKeepaliveIcon = () => {
542
- if (this.isSshDisabled()) {
543
- return null
544
- }
545
- const { keepaliveEnabled } = this.state
546
- const title = e('keepalive')
547
- const iconProps = {
548
- className: classnames('sess-icon pointer keepalive-icon', {
549
- active: keepaliveEnabled
550
- }),
551
- onClick: this.toggleKeepalive
552
- }
553
- return (
554
- <Tooltip title={title}>
555
- <HeartbeatIcon {...iconProps} />
556
- </Tooltip>
557
- )
558
- }
559
-
560
- renderBroadcastIcon = () => {
561
- if (
562
- this.isSshDisabled()
563
- ) {
564
- return null
565
- }
566
- const { broadcastInput } = this.state
567
- const title = e('broadcastInput')
568
- const iconProps = {
569
- className: classnames('sess-icon pointer broadcast-icon', {
570
- active: broadcastInput
571
- }),
572
- onClick: this.toggleBroadcastInput
573
- }
574
-
575
- return (
576
- <Tooltip title={title}>
577
- <ApartmentOutlined {...iconProps} />
578
- </Tooltip>
579
- )
580
- }
581
-
582
- renderTermControls = () => {
583
- const { props } = this
584
- const { pane } = props.tab
585
- if (pane !== paneMap.terminal) {
586
- return null
587
- }
588
- return (
589
- <div className='fright term-controls'>
590
- {this.fullscreenIcon()}
591
- {this.renderSearchIcon()}
592
- </div>
593
- )
594
- }
595
-
596
- renderSplitToggle = () => {
597
- if (window.store.isMobile) {
598
- return null
599
- }
600
- if (!this.canSplitView() || this.isNotTerminalType()) {
601
- return null
602
- }
603
- const title = e('sshSftpSplitView')
604
- const {
605
- sshSftpSplitView
606
- } = this.props.tab
607
- const cls = classnames(
608
- 'pointer sess-icon split-view-toggle',
609
- {
610
- active: sshSftpSplitView
611
- }
612
- )
613
- return (
614
- <Tooltip title={title} placement='bottomLeft'>
615
- <span
616
- className={cls}
617
- onClick={this.handleSshSftpSplitView}
618
- >
619
- <SplitViewIcon />
620
- </span>
621
- </Tooltip>
622
- )
623
- }
624
-
625
520
  isSsh = () => {
626
521
  const { tab } = this.props
627
522
  return tab.authType
628
523
  }
629
524
 
630
- renderPaneControl = () => {
631
- const {
632
- sshSftpSplitView
633
- } = this.props.tab
634
- if (this.isDisabled()) {
635
- return null
636
- }
637
- if (sshSftpSplitView && this.canSplitView()) {
638
- return null
639
- }
640
- const { props } = this
641
- const { tab } = props
642
- const { pane } = tab
643
- const termType = tab?.type
644
- const isSsh = tab.authType
645
- const isLocal = !isSsh && (termType === connectionMap.local || !termType)
646
- const types = [
647
- paneMap.terminal,
648
- paneMap.fileManager
649
- ]
650
- const controls = [
651
- isSsh ? paneMap.ssh : paneMap.terminal
652
- ]
653
- if (isSsh || isLocal) {
654
- controls.push(isSsh ? paneMap.sftp : paneMap.fileManager)
655
- }
656
- const simpleMapper = {
657
- [paneMap.terminal]: 'T',
658
- [paneMap.fileManager]: 'F',
659
- [paneMap.ssh]: 'T',
660
- [paneMap.sftp]: 'S'
661
- }
662
- return (
663
- <div className='term-sftp-tabs fleft'>
664
- {
665
- controls.map((type, i) => {
666
- const cls = classnames(
667
- 'type-tab',
668
- type,
669
- {
670
- active: types[i] === pane
671
- }
672
- )
673
- return (
674
- <span
675
- className={cls}
676
- key={type + '_' + i}
677
- onClick={() => this.onChangePane(types[i])}
678
- >
679
- <span className='type-tab-txt'>
680
- <span className='type-tab-full'>{e(type)}</span>
681
- <span className='type-tab-short'>{simpleMapper[type]}</span>
682
- <span className='type-tab-line' />
683
- </span>
684
- </span>
685
- )
686
- })
687
- }
688
- </div>
689
- )
690
- }
691
-
692
- renderSftpPathFollowControl = () => {
693
- if (this.isDisabled()) {
694
- return null
695
- }
696
- const {
697
- sftpPathFollowSsh
698
- } = this.state
699
- const { props } = this
700
- const { tab } = props
701
- const { pane, enableSsh, sshSftpSplitView } = tab
702
- const termType = tab?.type
703
- const isSsh = tab.authType
704
- const isLocal = !isSsh && (termType === connectionMap.local || !termType)
705
- const checkTxt = e('sftpPathFollowSsh')
706
- const checkProps = {
707
- onClick: this.toggleCheckSftpPathFollowSsh,
708
- className: classnames(
709
- 'sftp-follow-ssh-icon sess-icon pointer',
710
- {
711
- active: sftpPathFollowSsh
712
- }
713
- )
714
- }
715
- const isS = pane === paneMap.terminal ||
716
- sshSftpSplitView
717
- return (
718
- <>
719
- {
720
- (isSsh && enableSsh) || isLocal
721
- ? (
722
- <Tooltip title={checkTxt}>
723
- <span {...checkProps}>
724
- <PaperClipOutlined />
725
- </span>
726
- </Tooltip>
727
- )
728
- : null
729
- }
730
- {
731
- this.renderDelTip(isS)
732
- }
733
- </>
734
- )
735
- }
736
-
737
- renderControl = () => {
738
- if (
739
- this.isNotTerminalType()
740
- ) {
741
- return null
742
- }
743
- return (
744
- <div
745
- className='terminal-control fix'
746
- >
747
- {this.renderPaneControl()}
748
- {this.renderSftpPathFollowControl()}
749
- {this.renderSplitToggle()}
750
- {this.renderKeepaliveIcon()}
751
- {this.renderBroadcastIcon()}
752
- {this.renderTermControls()}
753
- </div>
754
- )
755
- }
756
-
757
525
  onSplitResize = (sizes) => {
758
526
  const direction = this.getSplitDirection()
759
527
  const {
@@ -850,7 +618,29 @@ export default class SessionWrapper extends Component {
850
618
  ref={this.domRef}
851
619
  {...divProps}
852
620
  >
853
- {this.renderControl()}
621
+ <SessionControl
622
+ tab={this.props.tab}
623
+ isMobile={window.store.isMobile}
624
+ isDisabled={this.isDisabled()}
625
+ isSshDisabled={this.isSshDisabled()}
626
+ isNotTerminalType={this.isNotTerminalType()}
627
+ canSplitView={this.canSplitView()}
628
+ sftpPathFollowSsh={this.state.sftpPathFollowSsh}
629
+ keepaliveEnabled={this.state.keepaliveEnabled}
630
+ broadcastInput={this.state.broadcastInput}
631
+ wrapDisabled={this.state.wrapDisabled}
632
+ delKeyPressed={this.state.delKeyPressed}
633
+ hideDelKeyTip={this.props.hideDelKeyTip}
634
+ onChangePane={(pane) => this.onChangePane(pane)}
635
+ toggleCheckSftpPathFollowSsh={this.toggleCheckSftpPathFollowSsh}
636
+ onSshSftpSplitView={this.handleSshSftpSplitView}
637
+ toggleKeepalive={this.toggleKeepalive}
638
+ toggleBroadcastInput={this.toggleBroadcastInput}
639
+ toggleWrap={this.toggleWrap}
640
+ onFullscreen={this.handleFullscreen}
641
+ onOpenSearch={this.handleOpenSearch}
642
+ onDismissDelKeyTip={this.handleDismissDelKeyTip}
643
+ />
854
644
  {this.renderViews()}
855
645
  </div>
856
646
  )
@@ -96,6 +96,7 @@
96
96
  &.active
97
97
  color var(--warn)
98
98
 
99
+ .wrap-toggle-icon
99
100
  .split-view-toggle
100
101
  &.active
101
102
  color var(--success)