@electerm/electerm-react 5.5.9 → 5.5.25
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.
- package/README.md +12 -2
- package/client/common/constants.js +14 -0
- package/client/common/default-setting.js +2 -2
- package/client/common/download.jsx +12 -0
- package/client/common/parse-vv.js +313 -0
- package/client/common/resolve.js +20 -14
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/components/ai/ai-history-item.jsx +1 -1
- package/client/components/ai/ai-history.jsx +1 -0
- package/client/components/ai/ai-history.styl +23 -0
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/color-picker.styl +3 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +137 -0
- package/client/components/bookmark-form/config/spice.js +1 -0
- package/client/components/common/remote-pointer.js +49 -0
- package/client/components/common/remote-session-shell.jsx +137 -0
- package/client/components/common/remote-session-shell.styl +80 -0
- package/client/components/common/responsive-tabs.jsx +89 -0
- package/client/components/common/responsive-tabs.styl +34 -0
- package/client/components/footer/footer-entry.jsx +39 -14
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +7 -1
- package/client/components/main/main.jsx +9 -2
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +23 -5
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/rdp/rdp-session.jsx +94 -124
- package/client/components/rdp/rdp.styl +13 -36
- package/client/components/session/session.styl +0 -16
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-modal.jsx +8 -3
- package/client/components/sftp/sftp-entry.jsx +26 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +45 -9
- package/client/components/sidebar/bookmark-select.jsx +3 -1
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/spice/spice-session.jsx +69 -72
- package/client/components/spice/spice.styl +10 -19
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +20 -23
- package/client/components/theme/theme-form.styl +8 -10
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/vnc/vnc-session.jsx +72 -104
- package/client/components/vnc/vnc.styl +25 -16
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +44 -0
- package/client/store/init-state.js +2 -1
- package/client/store/setting.js +4 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +9 -2
- package/package.json +1 -1
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
DownloadOutlined
|
|
15
15
|
} from '@ant-design/icons'
|
|
16
16
|
import {
|
|
17
|
-
Spin,
|
|
18
17
|
Select,
|
|
19
18
|
Tooltip
|
|
20
19
|
} from 'antd'
|
|
@@ -23,6 +22,8 @@ import scanCode from './code-scan'
|
|
|
23
22
|
import resolutions from './resolutions'
|
|
24
23
|
import { readClipboardAsync } from '../../common/clipboard'
|
|
25
24
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
25
|
+
import RemoteSessionShell from '../common/remote-session-shell'
|
|
26
|
+
import { eventToRemotePos } from '../common/remote-pointer'
|
|
26
27
|
import SwitchLabel from '../common/switch'
|
|
27
28
|
import HelpIcon from '../common/help-icon'
|
|
28
29
|
import Modal from '../common/modal'
|
|
@@ -102,6 +103,12 @@ export default class RdpSession extends PureComponent {
|
|
|
102
103
|
|
|
103
104
|
runInitScript = () => { }
|
|
104
105
|
|
|
106
|
+
// fullscreen always fills the screen, the per tab setting only applies
|
|
107
|
+
// windowed; the switch is not reachable in fullscreen anyway
|
|
108
|
+
getFit = () => {
|
|
109
|
+
return !!this.props.fullscreen || !!this.state.scaleViewport
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
setStatus = status => {
|
|
106
113
|
const id = this.props.tab?.id
|
|
107
114
|
this.props.editTab(id, {
|
|
@@ -458,30 +465,14 @@ export default class RdpSession extends PureComponent {
|
|
|
458
465
|
canvas.addEventListener('mousemove', (e) => {
|
|
459
466
|
if (!this.session) return
|
|
460
467
|
try {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const canvasRatio = canvas.width / canvas.height
|
|
470
|
-
let renderWidth, renderHeight
|
|
471
|
-
if (containerRatio > canvasRatio) {
|
|
472
|
-
renderHeight = rect.height
|
|
473
|
-
renderWidth = rect.height * canvasRatio
|
|
474
|
-
offsetX = (rect.width - renderWidth) / 2
|
|
475
|
-
} else {
|
|
476
|
-
renderWidth = rect.width
|
|
477
|
-
renderHeight = rect.width / canvasRatio
|
|
478
|
-
offsetY = (rect.height - renderHeight) / 2
|
|
479
|
-
}
|
|
480
|
-
scaleX = canvas.width / renderWidth
|
|
481
|
-
scaleY = canvas.height / renderHeight
|
|
482
|
-
}
|
|
483
|
-
const x = Math.round((e.clientX - rect.left - offsetX) * scaleX)
|
|
484
|
-
const y = Math.round((e.clientY - rect.top - offsetY) * scaleY)
|
|
468
|
+
// the canvas is rendered with object-fit: contain, so the pointer has
|
|
469
|
+
// to be mapped through the real surface rect (see remote-pointer.js)
|
|
470
|
+
const { x, y } = eventToRemotePos(
|
|
471
|
+
e,
|
|
472
|
+
canvas,
|
|
473
|
+
canvas.width,
|
|
474
|
+
canvas.height
|
|
475
|
+
)
|
|
485
476
|
const event = window.ironRdp.DeviceEvent.mouseMove(x, y)
|
|
486
477
|
const tx = new window.ironRdp.InputTransaction()
|
|
487
478
|
tx.addEvent(event)
|
|
@@ -705,12 +696,7 @@ export default class RdpSession extends PureComponent {
|
|
|
705
696
|
this.setupInputHandlers()
|
|
706
697
|
}
|
|
707
698
|
|
|
708
|
-
|
|
709
|
-
const contrlProps = this.getControlProps({
|
|
710
|
-
fixedPosition: false,
|
|
711
|
-
showExitFullscreen: false,
|
|
712
|
-
className: 'mg1l'
|
|
713
|
-
})
|
|
699
|
+
renderControlLeft = () => {
|
|
714
700
|
const {
|
|
715
701
|
id,
|
|
716
702
|
hasRemoteFiles,
|
|
@@ -730,61 +716,67 @@ export default class RdpSession extends PureComponent {
|
|
|
730
716
|
const uploadTitle = window.translate('upload') || 'Upload files to remote'
|
|
731
717
|
const downloadTitle = window.translate('download') || 'Download files from remote'
|
|
732
718
|
return (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
{
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
719
|
+
<>
|
|
720
|
+
<ReloadOutlined
|
|
721
|
+
onClick={this.handleReInit}
|
|
722
|
+
className='mg2r mg1l pointer'
|
|
723
|
+
/>
|
|
724
|
+
<Select
|
|
725
|
+
{...sleProps}
|
|
726
|
+
>
|
|
727
|
+
{
|
|
728
|
+
this.getAllRes().map(d => {
|
|
729
|
+
const v = d.id
|
|
730
|
+
return (
|
|
731
|
+
<Option
|
|
732
|
+
key={v}
|
|
733
|
+
value={v}
|
|
734
|
+
>
|
|
735
|
+
{d.width}x{d.height}
|
|
736
|
+
</Option>
|
|
737
|
+
)
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
</Select>
|
|
741
|
+
<EditOutlined
|
|
742
|
+
onClick={this.handleEditResolutions}
|
|
743
|
+
className='mg2r mg1l pointer'
|
|
744
|
+
/>
|
|
745
|
+
{this.renderInfo()}
|
|
746
|
+
<SwitchLabel
|
|
747
|
+
{...scaleProps}
|
|
748
|
+
/>
|
|
749
|
+
<Tooltip title={uploadTitle}>
|
|
750
|
+
<UploadOutlined
|
|
751
|
+
onClick={this.handleUploadButtonClick}
|
|
752
|
+
className={`mg1r mg2l pointer rdp-file-transfer-btn${uploadReady ? ' rdp-download-flash' : ''}`}
|
|
765
753
|
/>
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
</Tooltip>
|
|
772
|
-
<Tooltip title={downloadTitle}>
|
|
773
|
-
<DownloadOutlined
|
|
774
|
-
onClick={this.handleDownloadButtonClick}
|
|
775
|
-
className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
|
|
776
|
-
/>
|
|
777
|
-
</Tooltip>
|
|
778
|
-
<HelpIcon
|
|
779
|
-
link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
|
|
780
|
-
className='mg2r mg1l'
|
|
754
|
+
</Tooltip>
|
|
755
|
+
<Tooltip title={downloadTitle}>
|
|
756
|
+
<DownloadOutlined
|
|
757
|
+
onClick={this.handleDownloadButtonClick}
|
|
758
|
+
className={`mg2r mg1l pointer rdp-file-transfer-btn${hasRemoteFiles ? ' rdp-download-flash' : ' rdp-download-disabled'}`}
|
|
781
759
|
/>
|
|
782
|
-
</
|
|
783
|
-
<
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
760
|
+
</Tooltip>
|
|
761
|
+
<HelpIcon
|
|
762
|
+
link='https://github.com/electerm/electerm/wiki/RDP-File-Transfer'
|
|
763
|
+
className='mg2r mg1l'
|
|
764
|
+
/>
|
|
765
|
+
</>
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
renderControlRight = () => {
|
|
770
|
+
const contrlProps = this.getControlProps({
|
|
771
|
+
fixedPosition: false,
|
|
772
|
+
showExitFullscreen: false,
|
|
773
|
+
className: 'mg1l'
|
|
774
|
+
})
|
|
775
|
+
return (
|
|
776
|
+
<>
|
|
777
|
+
{this.props.fullscreenIcon()}
|
|
778
|
+
<RemoteFloatControl {...contrlProps} />
|
|
779
|
+
</>
|
|
788
780
|
)
|
|
789
781
|
}
|
|
790
782
|
|
|
@@ -802,48 +794,26 @@ export default class RdpSession extends PureComponent {
|
|
|
802
794
|
}
|
|
803
795
|
|
|
804
796
|
render () {
|
|
805
|
-
const { width
|
|
806
|
-
const { width, height, loading, scaleViewport } = this.state
|
|
807
|
-
const innerWidth = w - 10
|
|
808
|
-
const innerHeight = h - 80
|
|
809
|
-
const wrapperStyle = {
|
|
810
|
-
width: innerWidth + 'px',
|
|
811
|
-
height: innerHeight + 'px',
|
|
812
|
-
overflow: scaleViewport ? 'hidden' : 'auto'
|
|
813
|
-
}
|
|
814
|
-
const canvasProps = {
|
|
815
|
-
width,
|
|
816
|
-
height,
|
|
817
|
-
tabIndex: 0
|
|
818
|
-
}
|
|
819
|
-
const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
|
|
820
|
-
const sessProps = {
|
|
821
|
-
className: cls,
|
|
822
|
-
style: {
|
|
823
|
-
width: w + 'px',
|
|
824
|
-
height: h + 'px'
|
|
825
|
-
}
|
|
826
|
-
}
|
|
797
|
+
const { width, height, loading } = this.state
|
|
827
798
|
const controlProps = this.getControlProps()
|
|
828
799
|
return (
|
|
829
|
-
<
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
</Spin>
|
|
800
|
+
<RemoteSessionShell
|
|
801
|
+
loading={loading}
|
|
802
|
+
fit={this.getFit()}
|
|
803
|
+
wrapClassName='rdp-session-wrap'
|
|
804
|
+
controlLeft={this.renderControlLeft()}
|
|
805
|
+
controlRight={this.renderControlRight()}
|
|
806
|
+
floatControl={<RemoteFloatControl {...controlProps} />}
|
|
807
|
+
overlay={this.renderCredPrompt()}
|
|
808
|
+
>
|
|
809
|
+
<canvas
|
|
810
|
+
className='rdp-canvas'
|
|
811
|
+
width={width}
|
|
812
|
+
height={height}
|
|
813
|
+
tabIndex={0}
|
|
814
|
+
ref={this.canvasRef}
|
|
815
|
+
/>
|
|
816
|
+
</RemoteSessionShell>
|
|
847
817
|
)
|
|
848
818
|
}
|
|
849
819
|
}
|
|
@@ -1,36 +1,18 @@
|
|
|
1
|
+
// RDP only styling — the session layout (control bar, viewport, fit mode)
|
|
2
|
+
// lives in the shared remote session shell: common/remote-session-shell.styl
|
|
3
|
+
|
|
4
|
+
// fit: the canvas box fills the viewport and the remote surface is letterboxed
|
|
5
|
+
// inside it with object-fit: contain, so the aspect ratio is kept and the
|
|
6
|
+
// letterbox keeps the app background
|
|
1
7
|
.rdp-session-wrap
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
align-items: center
|
|
6
|
-
.session-v-info
|
|
7
|
-
position: absolute
|
|
8
|
-
top: 0
|
|
9
|
-
left: 0
|
|
10
|
-
width: 100%
|
|
11
|
-
&.scale-viewport
|
|
12
|
-
.rdp-scroll-wrapper canvas
|
|
13
|
-
width: 100% !important
|
|
14
|
-
height: 100% !important
|
|
15
|
-
object-fit: contain
|
|
8
|
+
&.fit
|
|
9
|
+
.rdp-canvas
|
|
10
|
+
object-fit contain
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
background var(--main-darker)
|
|
22
|
-
&::-webkit-scrollbar-track
|
|
23
|
-
background var(--main-darker)
|
|
24
|
-
box-shadow inset 0 0 5px var(--main-darker)
|
|
25
|
-
&::-webkit-scrollbar-thumb
|
|
26
|
-
background var(--primary)
|
|
27
|
-
border-radius 0
|
|
28
|
-
&::-webkit-scrollbar-corner
|
|
29
|
-
background var(--main-darker)
|
|
30
|
-
.rdp-scroll-wrapper
|
|
31
|
-
position relative
|
|
32
|
-
background var(--main)
|
|
33
|
-
z-index 299
|
|
12
|
+
// fit off: rendered at the remote desktop size, the viewport scrolls
|
|
13
|
+
.rdp-canvas
|
|
14
|
+
display block
|
|
15
|
+
outline none
|
|
34
16
|
|
|
35
17
|
.rdp-file-transfer-btn
|
|
36
18
|
font-size: 16px
|
|
@@ -45,9 +27,6 @@
|
|
|
45
27
|
&:hover
|
|
46
28
|
color: var(--text)
|
|
47
29
|
|
|
48
|
-
canvas
|
|
49
|
-
outline: none
|
|
50
|
-
|
|
51
30
|
@keyframes rdp-download-flash
|
|
52
31
|
0%, 100%
|
|
53
32
|
color: var(--primary)
|
|
@@ -58,5 +37,3 @@ canvas
|
|
|
58
37
|
|
|
59
38
|
.rdp-download-flash
|
|
60
39
|
animation: rdp-download-flash 1s ease-in-out infinite
|
|
61
|
-
|
|
62
|
-
|
|
@@ -71,22 +71,6 @@
|
|
|
71
71
|
overflow hidden
|
|
72
72
|
.session-v-info
|
|
73
73
|
line-height 30px
|
|
74
|
-
.vnc-scroll-wrapper
|
|
75
|
-
position relative
|
|
76
|
-
background var(--main)
|
|
77
|
-
z-index 299
|
|
78
|
-
&::-webkit-scrollbar
|
|
79
|
-
width 16px
|
|
80
|
-
height 16px
|
|
81
|
-
background var(--main-darker)
|
|
82
|
-
&::-webkit-scrollbar-track
|
|
83
|
-
background var(--main-darker)
|
|
84
|
-
box-shadow inset 0 0 5px var(--main-darker)
|
|
85
|
-
&::-webkit-scrollbar-thumb
|
|
86
|
-
background var(--primary)
|
|
87
|
-
border-radius 0
|
|
88
|
-
&::-webkit-scrollbar-corner
|
|
89
|
-
background var(--main-darker)
|
|
90
74
|
|
|
91
75
|
.not-split-view > .ant-splitter-bar .ant-splitter-bar-dragger
|
|
92
76
|
display none
|
|
@@ -9,7 +9,7 @@ import createName, { createTitleTag } from '../../common/create-title'
|
|
|
9
9
|
import classnames from 'classnames'
|
|
10
10
|
import { noop } from 'lodash-es'
|
|
11
11
|
import highlight from '../common/highlight'
|
|
12
|
-
import {
|
|
12
|
+
import { staticNewItemTabs, undeletableIds } from '../../common/constants'
|
|
13
13
|
import getInitItem from '../../common/init-setting-item'
|
|
14
14
|
|
|
15
15
|
const e = window.translate
|
|
@@ -64,7 +64,7 @@ export default class ItemList extends React.PureComponent {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
renderDelBtn = item => {
|
|
67
|
-
if (!item.id ||
|
|
67
|
+
if (!item.id || undeletableIds.has(item.id)) {
|
|
68
68
|
return null
|
|
69
69
|
}
|
|
70
70
|
const { shouldConfirmDel } = this.props
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { auto } from 'manate/react'
|
|
6
6
|
import { pick } from 'lodash-es'
|
|
7
|
-
import {
|
|
7
|
+
import { Spin } from 'antd'
|
|
8
8
|
import { lazy, Suspense } from 'react'
|
|
9
9
|
import SettingModal from './setting-wrap'
|
|
10
10
|
import LazyBoundary from '../common/lazy-boundary'
|
|
11
|
+
import ResponsiveTabs from '../common/responsive-tabs'
|
|
11
12
|
import {
|
|
12
13
|
settingMap,
|
|
13
14
|
modals,
|
|
@@ -132,11 +133,15 @@ export default auto(function SettingModalWrap (props) {
|
|
|
132
133
|
onChange: store.handleChangeSettingTab,
|
|
133
134
|
destroyOnHidden: true,
|
|
134
135
|
className: 'setting-tabs',
|
|
135
|
-
type: 'card'
|
|
136
|
+
type: 'card',
|
|
137
|
+
// the panel turns into a phone style drill-down at 800px (same
|
|
138
|
+
// breakpoint as the media query in setting-wrap.styl), collapse the tab
|
|
139
|
+
// bar there too instead of letting antd hide the overflow tabs
|
|
140
|
+
isMobile: store.innerWidth <= settingPanelMobileBreakpoint
|
|
136
141
|
}
|
|
137
142
|
return (
|
|
138
143
|
<>
|
|
139
|
-
<
|
|
144
|
+
<ResponsiveTabs
|
|
140
145
|
{...tabsProps}
|
|
141
146
|
/>
|
|
142
147
|
<LazyBoundary>
|
|
@@ -293,11 +293,29 @@ export default class Sftp extends Component {
|
|
|
293
293
|
if (!path && this.sftp) {
|
|
294
294
|
path = await this.getPwd(this.props.tab.username)
|
|
295
295
|
}
|
|
296
|
+
if (!path) {
|
|
297
|
+
// No sftp connection yet (still connecting, or the previous attempt
|
|
298
|
+
// failed). There is no remote home to resolve, and navigating to an
|
|
299
|
+
// unknown path would blank the panel, so (re)start the connection and
|
|
300
|
+
// let its own listing decide the path.
|
|
301
|
+
if (
|
|
302
|
+
!this.sftp &&
|
|
303
|
+
!this.state.loadingSftp &&
|
|
304
|
+
!this.state.remoteLoading
|
|
305
|
+
) {
|
|
306
|
+
this.initData(this.terminalId, this.port)
|
|
307
|
+
}
|
|
308
|
+
return
|
|
309
|
+
}
|
|
296
310
|
path = normalizeRemotePath(path)
|
|
297
311
|
} else {
|
|
298
312
|
path = this.getLocalHome()
|
|
299
313
|
}
|
|
300
314
|
|
|
315
|
+
if (typeof path !== 'string' || !path) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
301
319
|
this.setState({
|
|
302
320
|
[n]: path,
|
|
303
321
|
[nt]: path
|
|
@@ -1033,6 +1051,9 @@ export default class Sftp extends Component {
|
|
|
1033
1051
|
goParent = (type) => {
|
|
1034
1052
|
const n = `${type}Path`
|
|
1035
1053
|
const p = this.state[n]
|
|
1054
|
+
if (typeof p !== 'string' || !p) {
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
1036
1057
|
let np = resolve(p, '..')
|
|
1037
1058
|
if (type === typeMap.remote) {
|
|
1038
1059
|
np = normalizeRemotePath(np)
|
|
@@ -1136,6 +1157,11 @@ export default class Sftp extends Component {
|
|
|
1136
1157
|
|
|
1137
1158
|
renderParentItem = (type) => {
|
|
1138
1159
|
const currentPath = this.state[`${type}Path`]
|
|
1160
|
+
// The path can be momentarily unknown (e.g. the remote home while the sftp
|
|
1161
|
+
// client is still connecting). Show no parent item rather than throwing.
|
|
1162
|
+
if (typeof currentPath !== 'string') {
|
|
1163
|
+
return null
|
|
1164
|
+
}
|
|
1139
1165
|
const parentPath = resolve(currentPath, '..')
|
|
1140
1166
|
// Don't render parent item if we're at the root
|
|
1141
1167
|
if (parentPath === currentPath) {
|
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
position absolute
|
|
3
3
|
right 0
|
|
4
4
|
top 36px
|
|
5
|
-
|
|
5
|
+
// Unpinned is an overlay, so stop above the footer: the footer is app chrome
|
|
6
|
+
// and holds the very icon that toggles this panel, so a full-height overlay
|
|
7
|
+
// would bury its own trigger. 36px is footerHeight (common/constants.js).
|
|
8
|
+
// Two cases move or remove the footer, and are handled where their sibling
|
|
9
|
+
// rules already live — see shortcut-bar.styl (.shortcut-bar-on lifts the
|
|
10
|
+
// footer) and term-fullscreen.styl (rdp/vnc/spice fullscreen hides it).
|
|
11
|
+
bottom 36px
|
|
6
12
|
z-index 200
|
|
7
13
|
background var(--main)
|
|
8
14
|
color var(--text)
|
|
9
15
|
border-left 1px solid var(--main-darker)
|
|
16
|
+
// Pinned is a dock, not an overlay: it owns the whole right column, top to
|
|
17
|
+
// bottom. The layout squeezes the terminal out of the way instead
|
|
18
|
+
// (layout.jsx reserves rightPanelWidth), so covering the footer is intended.
|
|
10
19
|
&.right-side-panel-pinned
|
|
11
20
|
top 0
|
|
21
|
+
bottom 0
|
|
12
22
|
|
|
13
23
|
.drag-handle
|
|
14
24
|
left 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { memo, useRef } from 'react'
|
|
1
|
+
import React, { memo, useRef, useEffect } from 'react'
|
|
2
2
|
import DragHandle from '../common/drag-handle'
|
|
3
3
|
import './right-side-panel.styl'
|
|
4
4
|
import {
|
|
@@ -19,14 +19,44 @@ export default memo(function RightSidePanel (
|
|
|
19
19
|
rightPanelWidth,
|
|
20
20
|
children,
|
|
21
21
|
title,
|
|
22
|
-
rightPanelTab
|
|
22
|
+
rightPanelTab,
|
|
23
|
+
isMobile
|
|
23
24
|
}
|
|
24
25
|
) {
|
|
25
26
|
const panelRef = useRef(null)
|
|
26
27
|
|
|
28
|
+
// Esc closes the panel, but never at the terminal's expense: xterm forwards
|
|
29
|
+
// Esc to the remote shell, so stealing it there would break vim, less, etc.
|
|
30
|
+
// The guard checks the event target rather than focus state, because the
|
|
31
|
+
// keydown bubbles up from xterm's textarea inside .term-wrap — the same
|
|
32
|
+
// selector shortcut-bar-entry.jsx uses to detect terminal focus. A modal owns
|
|
33
|
+
// Esc while one is open. This hook has to stay above the early return below
|
|
34
|
+
// so the hook order never changes.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!rightPanelVisible) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
function onKeyDown (event) {
|
|
40
|
+
if (event.key !== 'Escape' || window.store.showModal) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const { target } = event
|
|
44
|
+
if (target && typeof target.closest === 'function' && target.closest('.term-wrap')) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
window.store.rightPanelVisible = false
|
|
48
|
+
}
|
|
49
|
+
document.addEventListener('keydown', onKeyDown)
|
|
50
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
51
|
+
}, [rightPanelVisible])
|
|
52
|
+
|
|
27
53
|
if (!rightPanelVisible) {
|
|
28
54
|
return null
|
|
29
55
|
}
|
|
56
|
+
// Mobile has no pin control (it is not rendered below), so a pin persisted
|
|
57
|
+
// from a desktop session must not turn the panel into a dock here — that
|
|
58
|
+
// would make it full height over the footer for no reachable reason.
|
|
59
|
+
const pinned = rightPanelPinned && !isMobile
|
|
30
60
|
const tag = rightPanelTab === 'ai'
|
|
31
61
|
? <Tag className='mg1r'>AI</Tag>
|
|
32
62
|
: <BarChartOutlined className='mg1r' />
|
|
@@ -46,11 +76,11 @@ export default memo(function RightSidePanel (
|
|
|
46
76
|
}
|
|
47
77
|
|
|
48
78
|
function togglePin () {
|
|
49
|
-
window.store.
|
|
79
|
+
window.store.setRightPanelPinned(!window.store.rightPanelPinned)
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
const panelProps = {
|
|
53
|
-
className: 'right-side-panel animate-fast' + (
|
|
83
|
+
className: 'right-side-panel animate-fast' + (pinned ? ' right-side-panel-pinned' : ''),
|
|
54
84
|
ref: panelRef,
|
|
55
85
|
style: {
|
|
56
86
|
width: `${rightPanelWidth}px`
|
|
@@ -58,7 +88,7 @@ export default memo(function RightSidePanel (
|
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
const pinProps = {
|
|
61
|
-
className: 'right-side-panel-pin right-side-panel-controls' + (
|
|
91
|
+
className: 'right-side-panel-pin right-side-panel-controls' + (pinned ? ' pinned' : ''),
|
|
62
92
|
onClick: togglePin
|
|
63
93
|
}
|
|
64
94
|
const dragProps = {
|
|
@@ -73,7 +103,11 @@ export default memo(function RightSidePanel (
|
|
|
73
103
|
<div
|
|
74
104
|
{...panelProps}
|
|
75
105
|
>
|
|
76
|
-
|
|
106
|
+
{/* Mobile panels are full-screen drawers (see store.rightPanelWidth):
|
|
107
|
+
their width is the viewport and is not adjustable, so the resize
|
|
108
|
+
handle is not rendered — it would otherwise sit on the screen's left
|
|
109
|
+
edge and drag a size the panel never uses. */}
|
|
110
|
+
{!isMobile && <DragHandle {...dragProps} />}
|
|
77
111
|
<Flex
|
|
78
112
|
className='right-panel-title pd2'
|
|
79
113
|
justify='space-between'
|
|
@@ -83,9 +117,11 @@ export default memo(function RightSidePanel (
|
|
|
83
117
|
{tag} {title}
|
|
84
118
|
</Typography.Text>
|
|
85
119
|
<Flex>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
{!isMobile && (
|
|
121
|
+
<PushpinOutlined
|
|
122
|
+
{...pinProps}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
89
125
|
<CloseCircleOutlined
|
|
90
126
|
className='right-side-panel-close right-side-panel-controls mg1l'
|
|
91
127
|
onClick={onClose}
|
|
@@ -20,7 +20,9 @@ export default auto(function BookmarkSelect (props) {
|
|
|
20
20
|
return null
|
|
21
21
|
}
|
|
22
22
|
const onClickItem = (item) => {
|
|
23
|
-
|
|
23
|
+
// A pinned (docked) panel stays open; anything else — including mobile,
|
|
24
|
+
// where pinning is not a reachable mode — dismisses on selection.
|
|
25
|
+
if (!(store.pinned && !store.isMobile)) {
|
|
24
26
|
store.setOpenedSideBar('')
|
|
25
27
|
}
|
|
26
28
|
store.onSelectBookmark(item.id)
|
|
@@ -43,17 +43,24 @@ export default function Sidebar (props) {
|
|
|
43
43
|
openWidgetsModal,
|
|
44
44
|
zoom,
|
|
45
45
|
leftSideBarIcons,
|
|
46
|
-
widgetInstancesLength
|
|
46
|
+
widgetInstancesLength,
|
|
47
|
+
isMobile
|
|
47
48
|
} = props
|
|
48
49
|
|
|
49
50
|
const { store } = window
|
|
50
51
|
|
|
52
|
+
// pinned is desktop-only, like the right panel's: on mobile the panel spans
|
|
53
|
+
// the viewport and there is no pin control to unpin it, so a pin persisted
|
|
54
|
+
// from a desktop session must not turn into a mode the user cannot leave.
|
|
55
|
+
const pinnedActive = pinned && !isMobile
|
|
56
|
+
|
|
51
57
|
const handleClickOutside = (event) => {
|
|
52
58
|
const { store } = window
|
|
53
59
|
// Nothing to dismiss when the panel is closed or pinned; also ignore
|
|
54
60
|
// clicks while typing in an input so the panel is not yanked away
|
|
55
|
-
// mid-interaction.
|
|
56
|
-
|
|
61
|
+
// mid-interaction. Read the pin live rather than from the render scope:
|
|
62
|
+
// this listener is attached once, so a captured value would go stale.
|
|
63
|
+
if (!store.openedSideBar || (store.pinned && !store.isMobile) || hasActiveInput()) {
|
|
57
64
|
return
|
|
58
65
|
}
|
|
59
66
|
const target = event.target
|
|
@@ -221,10 +228,12 @@ export default function Sidebar (props) {
|
|
|
221
228
|
setLeftSidePanelWidth={setLeftSidePanelWidth}
|
|
222
229
|
leftSidePanelWidth={leftSidePanelWidth}
|
|
223
230
|
leftSideBarWidth={leftSideBarWidth}
|
|
231
|
+
isMobile={isMobile}
|
|
224
232
|
>
|
|
225
233
|
<SideBarPanel
|
|
226
|
-
pinned={
|
|
234
|
+
pinned={pinnedActive}
|
|
227
235
|
sidebarPanelTab={sidebarPanelTab}
|
|
236
|
+
isMobile={isMobile}
|
|
228
237
|
/>
|
|
229
238
|
</SidePanel>
|
|
230
239
|
</div>
|
|
@@ -26,15 +26,20 @@ export default function SidePanel (props) {
|
|
|
26
26
|
onDragMove,
|
|
27
27
|
left: true
|
|
28
28
|
}
|
|
29
|
+
// Mobile panels are full-screen drawers: the width is the viewport and is not
|
|
30
|
+
// adjustable, so the resize handle is not rendered at all — a handle there
|
|
31
|
+
// would sit on the screen edge and drag a size the panel does not use (see
|
|
32
|
+
// store.leftSidePanelWidth, which ignores the stored width on mobile).
|
|
33
|
+
const dragHandle = props.isMobile
|
|
34
|
+
? null
|
|
35
|
+
: <DragHandle {...dragProps} />
|
|
29
36
|
return (
|
|
30
37
|
<div
|
|
31
38
|
{...props.sideProps}
|
|
32
39
|
ref={panelRef}
|
|
33
40
|
draggable={false}
|
|
34
41
|
>
|
|
35
|
-
|
|
36
|
-
{...dragProps}
|
|
37
|
-
/>
|
|
42
|
+
{dragHandle}
|
|
38
43
|
{props.children}
|
|
39
44
|
</div>
|
|
40
45
|
)
|