@electerm/electerm-react 2.12.0 → 2.13.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.
@@ -29,7 +29,10 @@ export const getFilePath = (file) => {
29
29
  export const getDropFileList = (dataTransfer) => {
30
30
  const fromFile = dataTransfer.getData('fromFile')
31
31
  if (fromFile) {
32
- return [JSON.parse(fromFile)]
32
+ const parsed = JSON.parse(fromFile)
33
+ return Array.isArray(parsed)
34
+ ? parsed
35
+ : [parsed]
33
36
  }
34
37
 
35
38
  const { files } = dataTransfer
@@ -0,0 +1,176 @@
1
+ import { autoRun } from 'manate'
2
+ import copy from 'json-deep-copy'
3
+ import uid from '../../common/uid'
4
+ import resolve from '../../common/resolve'
5
+ import fs from '../../common/fs'
6
+ import { typeMap } from '../../common/constants'
7
+ import { getFolderFromFilePath, getLocalFileInfo } from '../sftp/file-read'
8
+
9
+ export default class Remote2RemoteHandler {
10
+ constructor (props) {
11
+ this.props = props
12
+ this.id = uid()
13
+ }
14
+
15
+ get store () {
16
+ return window.store
17
+ }
18
+
19
+ get fromFile () {
20
+ return this.props.fromFile
21
+ }
22
+
23
+ get fromPath () {
24
+ const { path, name } = this.fromFile
25
+ return resolve(path, name)
26
+ }
27
+
28
+ get toPath () {
29
+ return this.props.toPath
30
+ }
31
+
32
+ buildTempPath = () => {
33
+ const { name, ext, base } = getFolderFromFilePath(this.fromPath, true)
34
+ const tail = uid()
35
+ const tempName = ext
36
+ ? `${base}-${tail}.${ext}`
37
+ : `${name}-${tail}`
38
+ return resolve(window.pre.tempDir, tempName)
39
+ }
40
+
41
+ buildStep1Transfer = () => {
42
+ const {
43
+ title,
44
+ tabType,
45
+ sourceTabId,
46
+ sourceHost
47
+ } = this.props
48
+ const transfer = {
49
+ id: uid(),
50
+ typeFrom: typeMap.remote,
51
+ typeTo: typeMap.local,
52
+ fromPath: this.fromPath,
53
+ toPath: this.tempPath,
54
+ tabId: sourceTabId,
55
+ host: sourceHost,
56
+ title,
57
+ tabType,
58
+ operation: '',
59
+ remote2remoteStep: 1,
60
+ remote2remoteId: this.id
61
+ }
62
+ return transfer
63
+ }
64
+
65
+ buildStep2Transfer = (fromFile) => {
66
+ const {
67
+ targetTabId,
68
+ targetHost,
69
+ targetTitle,
70
+ targetTabType
71
+ } = this.props
72
+ const transfer = {
73
+ id: uid(),
74
+ typeFrom: typeMap.local,
75
+ typeTo: typeMap.remote,
76
+ fromPath: this.tempPath,
77
+ toPath: this.toPath,
78
+ fromFile,
79
+ tabId: targetTabId,
80
+ host: targetHost,
81
+ title: targetTitle,
82
+ tabType: targetTabType,
83
+ operation: '',
84
+ remote2remoteStep: 2,
85
+ remote2remoteId: this.id,
86
+ originalId: this.step1Transfer?.id
87
+ }
88
+ return transfer
89
+ }
90
+
91
+ start = () => {
92
+ this.tempPath = this.buildTempPath()
93
+ this.step1Transfer = this.buildStep1Transfer()
94
+ this.store.addTransferList([copy(this.step1Transfer)])
95
+ this.startWatch()
96
+ }
97
+
98
+ startWatch = () => {
99
+ this.ref = autoRun(() => {
100
+ this.tick()
101
+ return this.store.transferHistory
102
+ })
103
+ this.ref.start()
104
+ }
105
+
106
+ stopWatch = () => {
107
+ this.ref?.stop()
108
+ this.ref = null
109
+ }
110
+
111
+ tick = async () => {
112
+ const step1 = this.findHistory(this.step1Transfer?.id)
113
+
114
+ if (!this.step2Transfer) {
115
+ if (this.creatingStep2) {
116
+ return
117
+ }
118
+ if (!step1) {
119
+ return
120
+ }
121
+ if (step1.error) {
122
+ return this.finish(step1.error)
123
+ }
124
+ this.creatingStep2 = true
125
+ const localFromFile = await getLocalFileInfo(this.tempPath).catch(() => null)
126
+ if (!localFromFile) {
127
+ this.creatingStep2 = false
128
+ return this.finish('local temp file/folder not found')
129
+ }
130
+ this.step2Transfer = this.buildStep2Transfer(localFromFile)
131
+ this.creatingStep2 = false
132
+ this.store.addTransferList([copy(this.step2Transfer)])
133
+ return
134
+ }
135
+
136
+ const step2 = this.findHistory(this.step2Transfer.id)
137
+ if (!step2) {
138
+ return
139
+ }
140
+
141
+ return this.finish(step2.error)
142
+ }
143
+
144
+ findHistory = (transferId) => {
145
+ if (!transferId) {
146
+ return null
147
+ }
148
+ return this.store.transferHistory.find(item => {
149
+ return item.id === transferId || item.originalId === transferId
150
+ })
151
+ }
152
+
153
+ cleanup = async () => {
154
+ if (!this.tempPath) {
155
+ return
156
+ }
157
+ await fs.rmrf(this.tempPath).catch(() => {})
158
+ }
159
+
160
+ finish = async (error) => {
161
+ if (this.finished) {
162
+ return
163
+ }
164
+ this.finished = true
165
+ this.stopWatch()
166
+ await this.cleanup()
167
+ this.props.onDone?.({
168
+ id: this.id,
169
+ error
170
+ })
171
+ }
172
+
173
+ stop = async () => {
174
+ await this.finish()
175
+ }
176
+ }
@@ -0,0 +1,81 @@
1
+ import { Component } from 'react'
2
+ import resolve from '../../common/resolve'
3
+ import { typeMap } from '../../common/constants'
4
+ import { refsStatic } from '../common/ref'
5
+ import Remote2RemoteHandler from './remote2remote-handler'
6
+
7
+ const handlerRefId = 'remote2remote-handlers'
8
+
9
+ export default class Remote2RemoteHandlers extends Component {
10
+ constructor (props) {
11
+ super(props)
12
+ this.handlers = new Map()
13
+ }
14
+
15
+ componentDidMount () {
16
+ refsStatic.add(handlerRefId, this)
17
+ }
18
+
19
+ componentWillUnmount () {
20
+ refsStatic.remove(handlerRefId)
21
+ this.handlers.forEach(handler => {
22
+ handler.stop()
23
+ })
24
+ this.handlers.clear()
25
+ }
26
+
27
+ canHandle = ({ fromFile, targetHost }) => {
28
+ return fromFile?.type === typeMap.remote &&
29
+ fromFile?.host &&
30
+ targetHost &&
31
+ fromFile.host !== targetHost &&
32
+ fromFile?.tabId
33
+ }
34
+
35
+ createHandler = ({ fromFile, targetPathBase, targetTab }) => {
36
+ const handler = new Remote2RemoteHandler({
37
+ fromFile,
38
+ toPath: resolve(targetPathBase, fromFile.name),
39
+ sourceHost: fromFile.host,
40
+ sourceTabId: fromFile.tabId,
41
+ title: fromFile.title,
42
+ tabType: fromFile.tabType,
43
+ targetHost: targetTab.host,
44
+ targetTabId: targetTab.id,
45
+ targetTitle: targetTab.title || targetTab.host,
46
+ targetTabType: targetTab.type,
47
+ onDone: this.onDone
48
+ })
49
+ this.handlers.set(handler.id, handler)
50
+ handler.start()
51
+ }
52
+
53
+ onDone = ({ id, error }) => {
54
+ this.handlers.delete(id)
55
+ if (error) {
56
+ window.store.onError(new Error(error))
57
+ }
58
+ }
59
+
60
+ onRemote2RemoteDrop = ({ fromFiles, toFile, targetTab }) => {
61
+ const targetPathBase = resolve(toFile.path, toFile.name)
62
+ const targetHost = targetTab?.host
63
+ let handled = false
64
+ for (const fromFile of fromFiles) {
65
+ if (!this.canHandle({ fromFile, targetHost })) {
66
+ continue
67
+ }
68
+ handled = true
69
+ this.createHandler({
70
+ fromFile,
71
+ targetPathBase,
72
+ targetTab
73
+ })
74
+ }
75
+ return handled
76
+ }
77
+
78
+ render () {
79
+ return null
80
+ }
81
+ }
@@ -14,6 +14,7 @@ import Resolutions from '../rdp/resolution-edit'
14
14
  import TerminalInteractive from '../terminal/terminal-interactive'
15
15
  import ConfirmModalStore from '../file-transfer/conflict-resolve.jsx'
16
16
  import TransferQueue from '../file-transfer/transfer-queue'
17
+ import Remote2RemoteHandlers from '../file-transfer/remote2remote-handlers.jsx'
17
18
  import TerminalCmdSuggestions from '../terminal/terminal-command-dropdown'
18
19
  import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
19
20
  import classnames from 'classnames'
@@ -280,6 +281,7 @@ export default auto(function Index (props) {
280
281
  {...conflictStoreProps}
281
282
  config={config}
282
283
  />
284
+ <Remote2RemoteHandlers />
283
285
  <Resolutions {...resProps} />
284
286
  <InfoModal {...infoModalProps} />
285
287
  <RightSidePanel {...rightPanelProps}>
@@ -26,7 +26,7 @@ export default function ProfileFormSsh (props) {
26
26
  {...formItemLayout}
27
27
  label={e('password')}
28
28
  hasFeedback
29
- name={['rdp', 'password']}
29
+ name={['ftp', 'password']}
30
30
  >
31
31
  <Password />
32
32
  </FormItem>
@@ -601,35 +601,44 @@ export default class RdpSession extends PureComponent {
601
601
 
602
602
  render () {
603
603
  const { width: w, height: h } = this.props
604
- const rdpProps = {
605
- style: {
606
- width: w + 'px',
607
- height: h + 'px'
608
- }
609
- }
610
604
  const { width, height, loading, scaleViewport } = this.state
605
+ const innerWidth = w - 10
606
+ const innerHeight = h - 80
607
+ const wrapperStyle = {
608
+ width: innerWidth + 'px',
609
+ height: innerHeight + 'px',
610
+ overflow: scaleViewport ? 'hidden' : 'auto'
611
+ }
611
612
  const canvasProps = {
612
613
  width,
613
614
  height,
614
615
  tabIndex: 0
615
616
  }
616
- if (scaleViewport) {
617
- canvasProps.className = 'scale-viewport'
618
- }
619
617
  const cls = `rdp-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
618
+ const sessProps = {
619
+ className: cls,
620
+ style: {
621
+ width: w + 'px',
622
+ height: h + 'px'
623
+ }
624
+ }
620
625
  const controlProps = this.getControlProps()
621
626
  return (
622
627
  <Spin spinning={loading}>
623
628
  <div
624
- {...rdpProps}
625
- className={cls}
629
+ {...sessProps}
626
630
  >
627
631
  {this.renderControl()}
628
- <canvas
629
- {...canvasProps}
630
- ref={this.canvasRef}
631
- />
632
632
  <RemoteFloatControl {...controlProps} />
633
+ <div
634
+ style={wrapperStyle}
635
+ className='rdp-scroll-wrapper s-scroll-wrapper'
636
+ >
637
+ <canvas
638
+ {...canvasProps}
639
+ ref={this.canvasRef}
640
+ />
641
+ </div>
633
642
  </div>
634
643
  </Spin>
635
644
  )
@@ -9,7 +9,27 @@
9
9
  left: 0
10
10
  width: 100%
11
11
  &.scale-viewport
12
- canvas
12
+ .rdp-scroll-wrapper canvas
13
13
  width: 100% !important
14
+ height: 100% !important
14
15
  object-fit: contain
15
16
 
17
+ .s-scroll-wrapper
18
+ &::-webkit-scrollbar
19
+ width 16px
20
+ height 16px
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
34
+
35
+
@@ -225,7 +225,21 @@ export default class FileSection extends React.Component {
225
225
  ? onDragCls + ' ' + onMultiDragCls
226
226
  : onDragCls
227
227
  addClass(this.domRef.current, cls)
228
- e.dataTransfer.setData('fromFile', JSON.stringify(this.props.file))
228
+ const transferProps = createTransferProps(this.props)
229
+ const selected = this.isSelected(this.props.file.id)
230
+ const dragFiles = selected
231
+ ? this.props.getSelectedFiles()
232
+ : [this.props.file]
233
+ const filesWithMeta = dragFiles.map(file => {
234
+ return {
235
+ ...file,
236
+ host: this.props.tab?.host,
237
+ tabType: this.props.tab?.type,
238
+ tabId: transferProps.tabId,
239
+ title: transferProps.title
240
+ }
241
+ })
242
+ e.dataTransfer.setData('fromFile', JSON.stringify(filesWithMeta))
229
243
  }
230
244
 
231
245
  getDropFileList = data => {
@@ -283,6 +297,23 @@ export default class FileSection extends React.Component {
283
297
  } = toFile
284
298
 
285
299
  let operation = ''
300
+ const targetHost = this.props.tab?.host
301
+ const isCrossHostRemoteDrop = !fromFileManager &&
302
+ fromType === typeMap.remote &&
303
+ toType === typeMap.remote &&
304
+ fromFiles.every(file => file?.host && file.host !== targetHost)
305
+
306
+ if (isCrossHostRemoteDrop) {
307
+ const handled = refsStatic.get('remote2remote-handlers')?.onRemote2RemoteDrop({
308
+ fromFiles,
309
+ toFile,
310
+ targetTab: this.props.tab
311
+ })
312
+ if (handled) {
313
+ return
314
+ }
315
+ }
316
+
286
317
  // same side and drop to file = drop to folder
287
318
  if (!fromFileManager && fromType === toType && !isDirectoryTo) {
288
319
  return
@@ -739,6 +770,7 @@ export default class FileSection extends React.Component {
739
770
  typeTo,
740
771
  fromPath: resolve(path, name),
741
772
  toPath,
773
+ fromFile: file,
742
774
  id: generate(),
743
775
  ...createTransferProps(this.props),
744
776
  operation
@@ -22,6 +22,61 @@ export const getFileExt = fileName => {
22
22
  }
23
23
  }
24
24
 
25
+ const modeDirectoryMask = 0o170000
26
+ const modeDirectoryValue = 0o040000
27
+
28
+ const toIsDirectory = (stat) => {
29
+ if (!stat) {
30
+ return false
31
+ }
32
+
33
+ if (typeof stat.isDirectory === 'function') {
34
+ return stat.isDirectory()
35
+ }
36
+
37
+ if (typeof stat.isDirectory === 'boolean') {
38
+ return stat.isDirectory
39
+ }
40
+
41
+ if (typeof stat.type === 'string') {
42
+ return stat.type === 'd'
43
+ }
44
+
45
+ if (typeof stat.type === 'number') {
46
+ return stat.type === 2
47
+ }
48
+
49
+ if (typeof stat.mode === 'number') {
50
+ return (stat.mode & modeDirectoryMask) === modeDirectoryValue
51
+ }
52
+
53
+ return false
54
+ }
55
+
56
+ const toIsSymbolicLink = (stat) => {
57
+ if (!stat) {
58
+ return false
59
+ }
60
+
61
+ if (typeof stat.isSymbolicLink === 'function') {
62
+ return stat.isSymbolicLink()
63
+ }
64
+
65
+ if (typeof stat.isSymbolicLink === 'boolean') {
66
+ return stat.isSymbolicLink
67
+ }
68
+
69
+ if (typeof stat.type === 'string') {
70
+ return stat.type === 'l'
71
+ }
72
+
73
+ if (typeof stat.type === 'number') {
74
+ return stat.type === 3
75
+ }
76
+
77
+ return false
78
+ }
79
+
25
80
  export const getFolderFromFilePath = (filePath, isRemote) => {
26
81
  const sep = isRemote ? '/' : window.pre.sep
27
82
  const arr = filePath.split(sep)
@@ -54,8 +109,8 @@ export const getLocalFileInfo = async (filePath) => {
54
109
  type: 'local',
55
110
  ...getFolderFromFilePath(filePath, false),
56
111
  id: generate(),
57
- isDirectory: statr.isDirectory,
58
- isSymbolicLink: stat.isSymbolicLink
112
+ isDirectory: toIsDirectory(statr),
113
+ isSymbolicLink: toIsSymbolicLink(stat)
59
114
  }
60
115
  }
61
116
 
@@ -71,6 +126,6 @@ export const getRemoteFileInfo = async (sftp, filePath) => {
71
126
  type: 'remote',
72
127
  ...getFolderFromFilePath(filePath, true),
73
128
  id: generate(),
74
- isDirectory: stat.isDirectory
129
+ isDirectory: toIsDirectory(stat)
75
130
  }
76
131
  }
@@ -269,20 +269,23 @@ export default class SpiceSession extends PureComponent {
269
269
  }
270
270
  const cls = `spice-session-wrap session-v-wrap${scaleViewport ? ' scale-viewport' : ''}`
271
271
  const contrlProps = this.getControlProps()
272
+ const sessProps = {
273
+ className: cls,
274
+ style: {
275
+ width: w + 'px',
276
+ height: h + 'px'
277
+ }
278
+ }
272
279
  return (
273
280
  <Spin spinning={loading}>
274
281
  <div
275
- className={cls}
276
- style={{
277
- width: w + 'px',
278
- height: h + 'px'
279
- }}
282
+ {...sessProps}
280
283
  >
281
284
  {this.renderControl()}
282
285
  <RemoteFloatControl {...contrlProps} />
283
286
  <div
284
287
  style={wrapperStyle}
285
- className='spice-scroll-wrapper'
288
+ className='spice-scroll-wrapper s-scroll-wrapper'
286
289
  >
287
290
  <div
288
291
  ref={this.domRef}
@@ -1,11 +1,29 @@
1
- .spice-session-wrap.scale-viewport
2
- canvas
3
- width: 100% !important
4
- object-fit: contain
5
- .spice-scroll-wrapper
6
- display block
7
- .spice-scroll-wrapper
1
+ .spice-session-wrap
8
2
  display: flex
9
3
  flex-direction: column
10
- justify-content: center
11
- align-items: center
4
+ align-items: center
5
+ .session-v-info
6
+ position: relative
7
+ width: 100%
8
+ z-index: 300
9
+ &.scale-viewport
10
+ .spice-scroll-wrapper
11
+ display flex
12
+ align-items center
13
+ justify-content center
14
+ > div
15
+ width 100% !important
16
+ height 100% !important
17
+ display flex
18
+ align-items center
19
+ justify-content center
20
+ canvas
21
+ width 100% !important
22
+ height 100% !important
23
+ max-width 100% !important
24
+ max-height 100% !important
25
+ object-fit contain
26
+ .spice-scroll-wrapper
27
+ position relative
28
+ background var(--main)
29
+ z-index 299
@@ -23,7 +23,6 @@ export default function BookmarkToolbar (props) {
23
23
  const {
24
24
  onNewBookmark,
25
25
  onNewBookmarkGroup,
26
- onImport,
27
26
  onExport,
28
27
  onSshConfigs,
29
28
  bookmarkGroups,
@@ -121,7 +120,12 @@ export default function BookmarkToolbar (props) {
121
120
  },
122
121
  {
123
122
  label: e('import'),
124
- onClick: onImport,
123
+ onClick: () => {
124
+ const fileInput = document.querySelector('.upload-bookmark-icon')
125
+ if (fileInput) {
126
+ fileInput.click()
127
+ }
128
+ },
125
129
  icon: <ImportOutlined />
126
130
  },
127
131
  {
@@ -680,10 +680,6 @@ export default class ItemListTree extends Component {
680
680
  )
681
681
  }
682
682
 
683
- handleImport = () => {
684
- document.querySelector('.upload-bookmark-icon input')?.click()
685
- }
686
-
687
683
  handleExport = () => {
688
684
  document.querySelector('.download-bookmark-icon')?.click()
689
685
  }
@@ -697,7 +693,6 @@ export default class ItemListTree extends Component {
697
693
  <NewButtonsGroup
698
694
  onNewBookmark={this.handleNewBookmark}
699
695
  onNewBookmarkGroup={this.handleNewBookmarkGroup}
700
- onImport={this.handleImport}
701
696
  onExport={this.handleExport}
702
697
  onSshConfigs={this.handleSshConfigs}
703
698
  bookmarkGroups={this.props.bookmarkGroups}
@@ -18,6 +18,7 @@ import Modal from '../common/modal'
18
18
  import { copy } from '../../common/clipboard'
19
19
  import VncForm from './vnc-form'
20
20
  import RemoteFloatControl from '../common/remote-float-control'
21
+ import './vnc.styl'
21
22
 
22
23
  // noVNC module imports — loaded dynamically
23
24
  async function loadVncModule () {
@@ -602,14 +603,17 @@ export default class VncSession extends PureComponent {
602
603
  className: 'vnc-session-wrap session-v-wrap'
603
604
  }
604
605
  const contrlProps = this.getControlProps()
606
+ const sessProps = {
607
+ className: 'vnc-session-wrap',
608
+ style: {
609
+ width: w + 'px',
610
+ height: h + 'px'
611
+ }
612
+ }
605
613
  return (
606
614
  <Spin spinning={loading}>
607
615
  <div
608
- className='rdp-session-wrap pd1'
609
- style={{
610
- width: w + 'px',
611
- height: h + 'px'
612
- }}
616
+ {...sessProps}
613
617
  >
614
618
  {this.renderControl()}
615
619
  <RemoteFloatControl
@@ -0,0 +1,17 @@
1
+ .vnc-session-wrap
2
+ display: flex
3
+ flex-direction: column
4
+ justify-content: center
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
16
+ > div
17
+ background: transparent !important
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.12.0",
3
+ "version": "2.13.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",