@electerm/electerm-react 2.7.8 → 2.8.6

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 (29) hide show
  1. package/client/common/pre.js +38 -11
  2. package/client/components/bookmark-form/config/rdp.js +1 -0
  3. package/client/components/bookmark-form/config/vnc.js +5 -0
  4. package/client/components/common/remote-float-control.jsx +79 -0
  5. package/client/components/common/remote-float-control.styl +28 -0
  6. package/client/components/layout/layout.jsx +2 -1
  7. package/client/components/main/main.jsx +3 -6
  8. package/client/components/main/term-fullscreen.styl +1 -10
  9. package/client/components/rdp/rdp-session.jsx +131 -17
  10. package/client/components/rdp/resolutions.js +6 -0
  11. package/client/components/session/session.jsx +4 -3
  12. package/client/components/session/session.styl +18 -5
  13. package/client/components/session/sessions.jsx +2 -1
  14. package/client/components/shortcuts/shortcut-control.jsx +5 -3
  15. package/client/components/shortcuts/shortcut-handler.js +4 -2
  16. package/client/components/terminal/attach-addon-custom.js +13 -0
  17. package/client/components/terminal/event-emitter.js +27 -0
  18. package/client/components/terminal/terminal.jsx +10 -297
  19. package/client/components/terminal/zmodem-client.js +385 -0
  20. package/client/components/terminal-info/data-cols-parser.jsx +3 -2
  21. package/client/components/terminal-info/network.jsx +3 -2
  22. package/client/components/vnc/vnc-session.jsx +398 -62
  23. package/client/css/basic.styl +3 -0
  24. package/client/store/event.js +2 -2
  25. package/client/store/init-state.js +1 -1
  26. package/package.json +1 -1
  27. package/client/common/byte-format.js +0 -14
  28. package/client/components/main/term-fullscreen-control.jsx +0 -21
  29. package/client/components/terminal/xterm-zmodem.js +0 -55
@@ -1,5 +1,4 @@
1
- import { createRef } from 'react'
2
- import RdpSession from '../rdp/rdp-session'
1
+ import { PureComponent, createRef } from 'react'
3
2
  import { createTerm } from '../terminal/terminal-apis'
4
3
  import deepCopy from 'json-deep-copy'
5
4
  import clone from '../../common/to-simple-obj'
@@ -9,30 +8,41 @@ import {
9
8
  } from '../../common/constants'
10
9
  import {
11
10
  Spin,
12
- Tag
11
+ Select
13
12
  } from 'antd'
13
+ import {
14
+ ReloadOutlined
15
+ } from '@ant-design/icons'
14
16
  import message from '../common/message'
15
17
  import Modal from '../common/modal'
16
- import * as ls from '../../common/safe-local-storage'
17
18
  import { copy } from '../../common/clipboard'
18
- import resolutions from '../rdp/resolutions'
19
- import RFB from '@novnc/novnc/core/rfb'
20
19
  import VncForm from './vnc-form'
20
+ import RemoteFloatControl from '../common/remote-float-control'
21
+
22
+ // noVNC module imports — loaded dynamically
23
+ async function loadVncModule () {
24
+ if (window.novnc) return
25
+ console.debug('[VNC-CLIENT] Loading noVNC module...')
26
+ const mod = await import('@novnc/novnc/core/rfb')
27
+ window.novnc = {
28
+ RFB: mod.default
29
+ }
30
+ console.debug('[VNC-CLIENT] noVNC module loaded')
31
+ }
21
32
 
33
+ const { Option } = Select
22
34
  const e = window.translate
23
35
 
24
- export default class VncSession extends RdpSession {
36
+ export default class VncSession extends PureComponent {
25
37
  constructor (props) {
26
- const id = `vnc-reso-${props.tab.host}`
27
- const resObj = ls.getItemJSON(id, resolutions[0])
28
38
  super(props)
29
39
  this.state = {
30
40
  types: [],
31
41
  showConfirm: false,
32
42
  loading: false,
33
- aspectRatio: 4 / 3,
34
43
  name: '',
35
- ...resObj
44
+ screens: [],
45
+ currentScreen: '0'
36
46
  }
37
47
  }
38
48
 
@@ -45,33 +55,125 @@ export default class VncSession extends RdpSession {
45
55
  componentWillUnmount () {
46
56
  this.rfb && this.rfb.disconnect()
47
57
  delete this.rfb
58
+ clearTimeout(this.timer)
59
+ }
60
+
61
+ setStatus = status => {
62
+ const id = this.props.tab?.id
63
+ this.props.editTab(id, {
64
+ status
65
+ })
66
+ }
67
+
68
+ getScreenSize = () => {
69
+ const { screens, currentScreen } = this.state
70
+ const currentScreenData = screens.find(s => s.id === currentScreen)
71
+ return {
72
+ remoteWidth: currentScreenData ? currentScreenData.width : null,
73
+ remoteHeight: currentScreenData ? currentScreenData.height : null
74
+ }
75
+ }
76
+
77
+ calcCanvasSize = () => {
78
+ const { width, height } = this.props
79
+ return {
80
+ width: width - 10,
81
+ height: height - 80
82
+ }
83
+ }
84
+
85
+ buildWsUrl = (port, type = 'rdp', extra = '') => {
86
+ const { host, tokenElecterm } = this.props.config
87
+ const { id } = this.props.tab
88
+ if (window.et.buildWsUrl) {
89
+ return window.et.buildWsUrl(
90
+ host,
91
+ port,
92
+ tokenElecterm,
93
+ id,
94
+ type,
95
+ extra
96
+ )
97
+ }
98
+ return `ws://${host}:${port}/${type}/${id}?token=${tokenElecterm}${extra}`
99
+ }
100
+
101
+ getControlProps = (options = {}) => {
102
+ const {
103
+ screens,
104
+ currentScreen
105
+ } = this.state
106
+ const {
107
+ fixedPosition = true,
108
+ showExitFullscreen = true,
109
+ className = ''
110
+ } = options
111
+
112
+ return {
113
+ isFullScreen: this.props.fullscreen,
114
+ onSendCtrlAltDel: this.handleSendCtrlAltDel,
115
+ screens: screens.length > 1 ? screens : [],
116
+ currentScreen,
117
+ onSelectScreen: this.handleSelectScreen,
118
+ fixedPosition,
119
+ showExitFullscreen,
120
+ className
121
+ }
122
+ }
123
+
124
+ renderControl = () => {
125
+ const contrlProps = this.getControlProps({
126
+ fixedPosition: false,
127
+ showExitFullscreen: false,
128
+ className: 'mg1l'
129
+ })
130
+ return (
131
+ <div className='pd1 fix session-v-info'>
132
+ <div className='fleft'>
133
+ <ReloadOutlined
134
+ onClick={this.handleReInit}
135
+ className='mg2r mg1l pointer'
136
+ />
137
+ {this.renderScreensSelect()}
138
+ {this.renderInfo()}
139
+ </div>
140
+ <div className='fright'>
141
+ {this.props.fullscreenIcon()}
142
+ <RemoteFloatControl {...contrlProps} />
143
+ </div>
144
+ </div>
145
+ )
48
146
  }
49
147
 
50
- // computeProps = () => {
51
- // const {
52
- // height,
53
- // width,
54
- // tabsHeight,
55
- // leftSidebarWidth,
56
- // pinned,
57
- // openedSideBar
58
- // } = this.props
59
- // return {
60
- // width: width - (pinned && openedSideBar ? leftSidebarWidth : 0),
61
- // height: height - tabsHeight
62
- // }
63
- // }
148
+ renderScreensSelect = () => {
149
+ const {
150
+ screens,
151
+ currentScreen
152
+ } = this.state
153
+ if (screens.length <= 1) {
154
+ return null
155
+ }
156
+ return (
157
+ <Select
158
+ value={currentScreen}
159
+ onChange={this.handleSelectScreen}
160
+ className='mg2r'
161
+ popupMatchSelectWidth={false}
162
+ >
163
+ {
164
+ screens.map(s => (
165
+ <Option key={s.id} value={s.id}>{s.name}</Option>
166
+ ))
167
+ }
168
+ </Select>
169
+ )
170
+ }
64
171
 
65
172
  remoteInit = async (term = this.term) => {
66
173
  this.setState({
67
174
  loading: true
68
175
  })
69
176
  const { config } = this.props
70
- const {
71
- host,
72
- tokenElecterm,
73
- server = ''
74
- } = config
75
177
  const { id } = this.props
76
178
  const tab = window.store.applyProfile(deepCopy(this.props.tab || {}))
77
179
  const {
@@ -80,6 +182,9 @@ export default class VncSession extends RdpSession {
80
182
  viewOnly = false,
81
183
  scaleViewport = true,
82
184
  clipViewport = false,
185
+ qualityLevel = 3, // 0-9, lower = faster performance
186
+ compressionLevel = 1, // 0-9, lower = faster performance
187
+ shared = true,
83
188
  username,
84
189
  password
85
190
  } = tab
@@ -107,21 +212,35 @@ export default class VncSession extends RdpSession {
107
212
  const { pid, port } = r
108
213
  this.pid = pid
109
214
  this.port = port
110
- const hs = server
111
- ? server.replace(/https?:\/\//, '')
112
- : `${host}:${port}`
113
- const pre = server.startsWith('https') ? 'wss' : 'ws'
114
- const { width, height } = this.state
115
- const wsUrl = `${pre}://${hs}/vnc/${pid}?token=${tokenElecterm}&width=${width}&height=${height}`
215
+
216
+ // Load VNC module if not already loaded
217
+ try {
218
+ await loadVncModule()
219
+ } catch (e) {
220
+ console.error('[VNC-CLIENT] Failed to load VNC module:', e)
221
+ this.setState({ loading: false })
222
+ this.setStatus(statusMap.error)
223
+ return
224
+ }
225
+
226
+ const { width, height } = this.calcCanvasSize()
227
+ const wsUrl = this.buildWsUrl(port, 'vnc', `&width=${width}&height=${height}`)
228
+ // When scaleViewport is false, we don't set fixed dimensions on the canvas
229
+ // so it can render at the actual remote screen size
116
230
  const vncOpts = {
117
231
  clipViewport,
118
232
  scaleViewport,
119
233
  viewOnly,
120
- style: {
121
- width: width + 'px',
122
- height: height + 'px',
123
- overflow: 'scroll'
124
- },
234
+ qualityLevel, // JPEG quality 0-9, lower = faster
235
+ compressionLevel, // Compression 0-9, lower = faster
236
+ shared, // Allow shared connections
237
+ style: scaleViewport
238
+ ? {
239
+ width: width + 'px',
240
+ height: height + 'px',
241
+ overflow: 'hidden'
242
+ }
243
+ : {},
125
244
  credentials: {}
126
245
  }
127
246
  if (username) {
@@ -130,11 +249,112 @@ export default class VncSession extends RdpSession {
130
249
  if (password) {
131
250
  vncOpts.credentials.password = password
132
251
  }
252
+ const RFB = window.novnc.RFB
133
253
  const rfb = new RFB(
134
254
  this.getDom(),
135
255
  wsUrl,
136
256
  vncOpts
137
257
  )
258
+
259
+ // Monkey patch _handleExtendedDesktopSize to capture screen info
260
+ const originalHandleExtendedDesktopSize = rfb._handleExtendedDesktopSize
261
+ rfb._handleExtendedDesktopSize = function () {
262
+ try {
263
+ // Wait for header
264
+ if (this._sock.rQwait('ExtendedDesktopSize', 4)) {
265
+ return false
266
+ }
267
+
268
+ const numberOfScreens = this._sock.rQpeek8()
269
+ const bytes = 4 + (numberOfScreens * 16)
270
+ if (this._sock.rQwait('ExtendedDesktopSize', bytes)) {
271
+ return false
272
+ }
273
+
274
+ const firstUpdate = !this._supportsSetDesktopSize
275
+ this._supportsSetDesktopSize = true
276
+
277
+ if (firstUpdate) {
278
+ this._requestRemoteResize()
279
+ }
280
+
281
+ const num = this._sock.rQshift8() // number-of-screens
282
+ this._sock.rQskipBytes(3) // padding
283
+
284
+ const screens = []
285
+ for (let i = 0; i < num; i += 1) {
286
+ const idBytes = this._sock.rQshiftBytes(4)
287
+ const id = (idBytes[0] << 24 | idBytes[1] << 16 | idBytes[2] << 8 | idBytes[3]) >>> 0
288
+
289
+ const x = this._sock.rQshift16() // x-position
290
+ const y = this._sock.rQshift16() // y-position
291
+ const w = this._sock.rQshift16() // width
292
+ const h = this._sock.rQshift16() // height
293
+
294
+ const flagsBytes = this._sock.rQshiftBytes(4)
295
+ const flags = (flagsBytes[0] << 24 | flagsBytes[1] << 16 | flagsBytes[2] << 8 | flagsBytes[3]) >>> 0
296
+
297
+ screens.push({ id, x, y, width: w, height: h, flags })
298
+
299
+ if (i === 0) {
300
+ this._screenID = idBytes
301
+ this._screenFlags = flagsBytes
302
+ }
303
+ }
304
+
305
+ // Store the screens in a custom property and dispatch event
306
+ this.screens = screens
307
+ this.dispatchEvent(new CustomEvent('screens', { detail: screens }))
308
+
309
+ // Handle the resize feedback logic from original code
310
+ if (this._FBU.x === 1 && this._FBU.y !== 0) {
311
+ let msg = ''
312
+ switch (this._FBU.y) {
313
+ case 1: msg = 'Resize is administratively prohibited'; break
314
+ case 2: msg = 'Out of resources'; break
315
+ case 3: msg = 'Invalid screen layout'; break
316
+ default: msg = 'Unknown reason'; break
317
+ }
318
+ console.warn('Server did not accept the resize request: ' + msg)
319
+ } else {
320
+ this._resize(this._FBU.width, this._FBU.height)
321
+ }
322
+
323
+ return true
324
+ } catch (e) {
325
+ console.error('Error in patched _handleExtendedDesktopSize', e)
326
+ return originalHandleExtendedDesktopSize.call(this)
327
+ }
328
+ }
329
+
330
+ // Monkey patch autoscale to support single screen scaling
331
+ const originalAutoscale = rfb._display.autoscale
332
+ rfb._display.autoscale = (containerWidth, containerHeight) => {
333
+ const { currentScreen, screens } = this.state
334
+
335
+ if (currentScreen === 'all' || !screens.length) {
336
+ return originalAutoscale.call(rfb._display, containerWidth, containerHeight)
337
+ }
338
+
339
+ const screen = screens.find(s => s.id === currentScreen)
340
+ if (!screen) {
341
+ return originalAutoscale.call(rfb._display, containerWidth, containerHeight)
342
+ }
343
+
344
+ let scaleRatio = 1.0
345
+ if (containerWidth > 0 && containerHeight > 0) {
346
+ const targetAspectRatio = containerWidth / containerHeight
347
+ const screenAspectRatio = screen.width / screen.height
348
+
349
+ if (screenAspectRatio >= targetAspectRatio) {
350
+ scaleRatio = containerWidth / screen.width
351
+ } else {
352
+ scaleRatio = containerHeight / screen.height
353
+ }
354
+ }
355
+ rfb._display._rescale(scaleRatio)
356
+ }
357
+
138
358
  const events = [
139
359
  'connect',
140
360
  'disconnect',
@@ -143,21 +363,29 @@ export default class VncSession extends RdpSession {
143
363
  'clipboard',
144
364
  'bell',
145
365
  'desktopname',
146
- 'capabilities'
366
+ 'capabilities',
367
+ 'screens',
368
+ 'desktopsize'
147
369
  ]
148
370
  for (const event of events) {
149
371
  rfb.addEventListener(event, this[`on${window.capitalizeFirstLetter(event)}`])
150
372
  }
373
+ rfb.scaleViewport = scaleViewport
374
+ rfb.clipViewport = clipViewport
151
375
  this.rfb = rfb
152
376
  }
153
377
 
154
378
  onConnect = (event) => {
155
379
  this.setStatus(statusMap.success)
380
+ // Capture the remote desktop size from the RFB connection
156
381
  this.setState({
157
382
  loading: false
158
383
  })
159
384
  }
160
385
 
386
+ onDesktopsize = (event) => {
387
+ }
388
+
161
389
  onDisconnect = () => {
162
390
  this.setStatus(statusMap.error)
163
391
  }
@@ -201,10 +429,97 @@ export default class VncSession extends RdpSession {
201
429
  this.setState({
202
430
  name: event?.detail?.name || ''
203
431
  })
432
+ this.checkScreens()
204
433
  }
205
434
 
206
435
  onCapabilities = (capabilities) => {
207
- console.log('onCapabilities', capabilities)
436
+ setTimeout(this.checkScreens, 1000)
437
+ }
438
+
439
+ processScreens = (screens) => {
440
+ if (!screens || !screens.length) {
441
+ return []
442
+ }
443
+ return screens.map((s, i) => ({
444
+ name: `Screen ${i + 1}`,
445
+ ...s,
446
+ id: s.id !== undefined ? s.id : i
447
+ }))
448
+ }
449
+
450
+ onScreens = (event) => {
451
+ const screensData = event.detail
452
+ if (screensData && screensData.length) {
453
+ const screens = this.processScreens(screensData)
454
+ this.setState({
455
+ screens,
456
+ currentScreen: screens[0].id
457
+ })
458
+ }
459
+ }
460
+
461
+ checkScreens = () => {
462
+ // Attempt to detect screens from noVNC
463
+ let screens = this.rfb?.screens
464
+ if (!screens && this.rfb?.capabilities?.screens) {
465
+ screens = this.rfb.capabilities.screens
466
+ }
467
+
468
+ if (screens && screens.length) {
469
+ this.setState({
470
+ screens: this.processScreens(screens)
471
+ })
472
+ }
473
+ }
474
+
475
+ componentDidUpdate (prevProps, prevState) {
476
+ if (
477
+ prevProps.width !== this.props.width ||
478
+ prevProps.height !== this.props.height ||
479
+ prevState.currentScreen !== this.state.currentScreen
480
+ ) {
481
+ this.updateViewLayout()
482
+ }
483
+ }
484
+
485
+ updateViewLayout = () => {
486
+ const { currentScreen, screens } = this.state
487
+ const { scaleViewport = true, clipViewport = false } = this.props.tab
488
+ const container = this.domRef.current ? this.domRef.current.firstElementChild : null
489
+
490
+ if (!this.rfb || !container) {
491
+ return
492
+ }
493
+
494
+ // Force re-apply scaleViewport to trigger autoscale logic (if enabled)
495
+ // or reset to 1.0 (if disabled)
496
+ this.rfb.scaleViewport = scaleViewport
497
+ this.rfb.clipViewport = clipViewport
498
+
499
+ // Explicitly trigger autoscale to ensure our patched logic runs
500
+ if (scaleViewport && this.rfb._display) {
501
+ this.rfb._display.autoscale(container.clientWidth, container.clientHeight)
502
+ }
503
+
504
+ // Handle scrolling to the selected screen
505
+ this.timer = setTimeout(() => {
506
+ if (currentScreen === 'all') {
507
+ container.scrollTo(0, 0)
508
+ } else {
509
+ const screen = screens.find(s => s.id === currentScreen)
510
+ if (screen) {
511
+ const scale = this.rfb._display.scale
512
+ container.scrollTo(screen.x * scale, screen.y * scale)
513
+ }
514
+ }
515
+ }, 50)
516
+ }
517
+
518
+ handleSelectScreen = (id) => {
519
+ this.setState({
520
+ currentScreen: id
521
+ })
522
+ // updateViewLayout will be called by componentDidUpdate
208
523
  }
209
524
 
210
525
  getDom = () => {
@@ -233,10 +548,8 @@ export default class VncSession extends RdpSession {
233
548
  )
234
549
  }
235
550
 
236
- renderHelp = () => {
237
- return (
238
- <Tag color='red' className='mg1l' variant='solid'>Beta</Tag>
239
- )
551
+ handleSendCtrlAltDel = () => {
552
+ this.rfb?.sendCtrlAltDel()
240
553
  }
241
554
 
242
555
  renderConfirm () {
@@ -263,31 +576,54 @@ export default class VncSession extends RdpSession {
263
576
 
264
577
  render () {
265
578
  const { width: w, height: h } = this.props
266
- const vncProps = {
267
- style: {
268
- width: w + 'px',
269
- height: h + 'px'
270
- }
579
+ const { loading } = this.state
580
+ const { remoteWidth, remoteHeight } = this.getScreenSize()
581
+ const { scaleViewport = true } = this.props.tab
582
+ // When not in scale mode, we need a wrapper with container size,
583
+ // and the inner div should have remote screen size to show scrollbars
584
+ const isScaled = scaleViewport
585
+
586
+ const {
587
+ width: innerWidth,
588
+ height: innerHeight
589
+ } = this.calcCanvasSize()
590
+ const wrapperStyle = {
591
+ width: innerWidth + 'px',
592
+ height: innerHeight + 'px',
593
+ overflow: isScaled ? 'hidden' : 'auto'
594
+ }
595
+ const remoteStyle = {
596
+ width: isScaled ? '100%' : remoteWidth + 'px',
597
+ height: isScaled ? '100%' : remoteHeight + 'px',
598
+ overflow: 'hidden'
271
599
  }
272
- const { width, height, loading } = this.state
273
600
  const divProps = {
274
- style: {
275
- width: width + 'px',
276
- height: height + 'px'
277
- }
601
+ style: remoteStyle,
602
+ className: 'vnc-session-wrap session-v-wrap'
278
603
  }
604
+ const contrlProps = this.getControlProps()
279
605
  return (
280
606
  <Spin spinning={loading}>
281
607
  <div
282
- {...vncProps}
283
608
  className='rdp-session-wrap pd1'
609
+ style={{
610
+ width: w + 'px',
611
+ height: h + 'px'
612
+ }}
284
613
  >
285
614
  {this.renderControl()}
286
- <div
287
- {...divProps}
288
- className='vnc-session-wrap session-v-wrap'
289
- ref={this.domRef}
615
+ <RemoteFloatControl
616
+ {...contrlProps}
290
617
  />
618
+ <div
619
+ style={wrapperStyle}
620
+ className='vnc-scroll-wrapper'
621
+ >
622
+ <div
623
+ {...divProps}
624
+ ref={this.domRef}
625
+ />
626
+ </div>
291
627
  {this.renderConfirm()}
292
628
  </div>
293
629
  </Spin>
@@ -25,6 +25,9 @@ body
25
25
  background-color var(--primary)
26
26
  outline none
27
27
 
28
+ ::-webkit-scrollbar-corner
29
+ background-color transparent
30
+
28
31
  .common-err
29
32
  max-height 100px
30
33
  overflow-y scroll
@@ -37,8 +37,8 @@ export default Store => {
37
37
  window.dispatchEvent(new Event('resize'))
38
38
  }
39
39
 
40
- Store.prototype.toggleTermFullscreen = function (terminalFullScreen) {
41
- window.store.terminalFullScreen = terminalFullScreen
40
+ Store.prototype.toggleSessFullscreen = function (fullscreen) {
41
+ window.store.fullscreen = fullscreen
42
42
  setTimeout(window.store.triggerResize, 500)
43
43
  }
44
44
  }
@@ -193,7 +193,7 @@ export default () => {
193
193
  height: 500,
194
194
  isMaximized: window.pre.runSync('isMaximized'),
195
195
  hasNodePty: window.pre.runSync('nodePtyCheck'),
196
- terminalFullScreen: false,
196
+ fullscreen: false,
197
197
  hideDelKeyTip: ls.getItem(dismissDelKeyTipLsKey) === 'y',
198
198
  tabsHeight: 36,
199
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.7.8",
3
+ "version": "2.8.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,14 +0,0 @@
1
- export function formatBytes (sizeInKB) {
2
- if (!sizeInKB) {
3
- return ''
4
- }
5
- const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
6
- let index = 0
7
-
8
- while (sizeInKB >= 1024 && index < units.length - 1) {
9
- sizeInKB /= 1024
10
- index++
11
- }
12
-
13
- return `${sizeInKB.toFixed(2)} ${units[index]}`
14
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * btns
3
- */
4
-
5
- import { FullscreenExitOutlined } from '@ant-design/icons'
6
- import './term-fullscreen.styl'
7
-
8
- export default function TermFullscreenControl (props) {
9
- const handleExitFullscreen = () => {
10
- window.store.toggleTermFullscreen(false)
11
- }
12
- if (!props.terminalFullScreen) {
13
- return null
14
- }
15
- return (
16
- <FullscreenExitOutlined
17
- className='mg1r icon-info font16 pointer spliter term-fullscreen-control'
18
- onClick={handleExitFullscreen}
19
- />
20
- )
21
- }