@electerm/electerm-react 1.38.86 → 1.39.5

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 (34) hide show
  1. package/client/common/constants.js +6 -1
  2. package/client/common/default-setting.js +1 -0
  3. package/client/components/bookmark-form/bookmark-form.styl +8 -3
  4. package/client/components/bookmark-form/form-ssh-common.jsx +23 -13
  5. package/client/components/bookmark-form/index.jsx +6 -2
  6. package/client/components/bookmark-form/rdp-form-ui.jsx +175 -0
  7. package/client/components/bookmark-form/rdp-form.jsx +16 -0
  8. package/client/components/bookmark-form/ssh-form.jsx +5 -8
  9. package/client/components/bookmark-form/web-form-ui.jsx +32 -2
  10. package/client/components/footer/footer-entry.jsx +7 -1
  11. package/client/components/main/main.jsx +3 -0
  12. package/client/components/rdp/code-scan.js +155 -0
  13. package/client/components/rdp/rdp-session.jsx +439 -0
  14. package/client/components/rdp/resolution-edit.jsx +63 -0
  15. package/client/components/rdp/resolution-form.jsx +134 -0
  16. package/client/components/rdp/resolutions.js +110 -0
  17. package/client/components/session/session.jsx +40 -5
  18. package/client/components/session/session.styl +4 -1
  19. package/client/components/session/sessions.jsx +2 -1
  20. package/client/components/setting-panel/setting-common.jsx +1 -0
  21. package/client/components/sftp/file-props-modal.jsx +1 -1
  22. package/client/components/sftp/sftp-entry.jsx +2 -1
  23. package/client/components/terminal/index.jsx +7 -0
  24. package/client/components/terminal/term-search.jsx +12 -0
  25. package/client/entry/rle.js +2 -0
  26. package/client/entry/rle.wasm +0 -0
  27. package/client/store/common.js +5 -0
  28. package/client/store/index.js +2 -1
  29. package/client/store/init-state.js +4 -1
  30. package/client/store/load-data.js +1 -1
  31. package/client/store/watch.js +6 -0
  32. package/client/views/index.pug +4 -0
  33. package/package.json +1 -1
  34. /package/client/components/{session → web}/web-session.jsx +0 -0
@@ -0,0 +1,439 @@
1
+ import { Component } from '../common/react-subx'
2
+ import { createTerm } from '../terminal/terminal-apis'
3
+ import deepCopy from 'json-deep-copy'
4
+ import clone from '../../common/to-simple-obj'
5
+ import { handleErr } from '../../common/fetch'
6
+ import {
7
+ statusMap,
8
+ rdpHelpLink
9
+ } from '../../common/constants'
10
+ import {
11
+ notification,
12
+ Spin,
13
+ // Button,
14
+ Select
15
+ } from 'antd'
16
+ import {
17
+ ReloadOutlined,
18
+ EditOutlined
19
+ } from '@ant-design/icons'
20
+ import HelpIcon from '../common/help-icon'
21
+ import * as ls from '../../common/safe-local-storage'
22
+ import scanCode from './code-scan'
23
+ import resolutions from './resolutions'
24
+
25
+ const { Option } = Select
26
+ // const { prefix } = window
27
+ // const e = prefix('ssh')
28
+ // const m = prefix('menu')
29
+
30
+ export default class RdpSession extends Component {
31
+ constructor (props) {
32
+ const id = `rdp-reso-${props.tab.host}`
33
+ const resObj = ls.getItemJSON(id, resolutions[0])
34
+ super(props)
35
+ this.state = {
36
+ loading: false,
37
+ bitmapProps: {},
38
+ aspectRatio: 4 / 3,
39
+ ...resObj
40
+ }
41
+ }
42
+
43
+ componentDidMount () {
44
+ this.remoteInit()
45
+ }
46
+
47
+ componentWillUnmount () {
48
+ this.socket && this.socket.close()
49
+ delete this.socket
50
+ }
51
+
52
+ runInitScript = () => {
53
+
54
+ }
55
+
56
+ setStatus = status => {
57
+ const id = this.props.tab?.id
58
+ this.props.editTab(id, {
59
+ status
60
+ })
61
+ }
62
+
63
+ computeProps = () => {
64
+ const {
65
+ height,
66
+ width,
67
+ tabsHeight,
68
+ leftSidebarWidth,
69
+ pinned,
70
+ openedSideBar
71
+ } = this.props
72
+ return {
73
+ width: width - (pinned && openedSideBar ? leftSidebarWidth : 0),
74
+ height: height - tabsHeight
75
+ }
76
+ }
77
+
78
+ remoteInit = async (term = this.term) => {
79
+ this.setState({
80
+ loading: true
81
+ })
82
+ const { config } = this.props
83
+ const {
84
+ host,
85
+ port,
86
+ tokenElecterm,
87
+ server = ''
88
+ } = config
89
+ const { sessionId, id } = this.props
90
+ const tab = deepCopy(this.props.tab || {})
91
+ const {
92
+ type,
93
+ term: terminalType
94
+ } = tab
95
+ const opts = clone({
96
+ term: terminalType || config.terminalType,
97
+ sessionId,
98
+ tabId: id,
99
+ srcTabId: tab.id,
100
+ termType: type,
101
+ ...tab
102
+ })
103
+ let pid = await createTerm(opts)
104
+ .catch(err => {
105
+ const text = err.message
106
+ handleErr({ message: text })
107
+ })
108
+ pid = pid || ''
109
+ this.setState({
110
+ loading: false
111
+ })
112
+ if (!pid) {
113
+ this.setStatus(statusMap.error)
114
+ return
115
+ }
116
+ this.setStatus(statusMap.success)
117
+ this.pid = pid
118
+ const hs = server
119
+ ? server.replace(/https?:\/\//, '')
120
+ : `${host}:${port}`
121
+ const pre = server.startsWith('https') ? 'wss' : 'ws'
122
+ const { width, height } = this.state
123
+ const wsUrl = `${pre}://${hs}/rdp/${pid}?sessionId=${sessionId}&token=${tokenElecterm}&width=${width}&height=${height}`
124
+ const socket = new WebSocket(wsUrl)
125
+ socket.onclose = this.oncloseSocket
126
+ socket.onerror = this.onerrorSocket
127
+ this.socket = socket
128
+ socket.onopen = this.runInitScript
129
+ socket.onmessage = this.onData
130
+ }
131
+
132
+ decompress = (bitmap) => {
133
+ let fName = null
134
+ switch (bitmap.bitsPerPixel) {
135
+ case 15:
136
+ fName = 'bitmap_decompress_15'
137
+ break
138
+ case 16:
139
+ fName = 'bitmap_decompress_16'
140
+ break
141
+ case 24:
142
+ fName = 'bitmap_decompress_24'
143
+ break
144
+ case 32:
145
+ fName = 'bitmap_decompress_32'
146
+ break
147
+ default:
148
+ throw new Error('invalid bitmap data format')
149
+ }
150
+ const rle = window.Module
151
+ const input = new Uint8Array(bitmap.data.data)
152
+ const inputPtr = rle._malloc(input.length)
153
+ const inputHeap = new Uint8Array(rle.HEAPU8.buffer, inputPtr, input.length)
154
+ inputHeap.set(input)
155
+
156
+ const outputWidth = bitmap.destRight - bitmap.destLeft + 1
157
+ const outputHeight = bitmap.destBottom - bitmap.destTop + 1
158
+ const ouputSize = outputWidth * outputHeight * 4
159
+ const outputPtr = rle._malloc(ouputSize)
160
+
161
+ const outputHeap = new Uint8Array(rle.HEAPU8.buffer, outputPtr, ouputSize)
162
+
163
+ rle.ccall(fName,
164
+ 'number',
165
+ ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
166
+ [outputHeap.byteOffset, outputWidth, outputHeight, bitmap.width, bitmap.height, inputHeap.byteOffset, input.length]
167
+ )
168
+
169
+ const output = new Uint8ClampedArray(outputHeap.buffer, outputHeap.byteOffset, ouputSize)
170
+
171
+ rle._free(inputPtr)
172
+ rle._free(outputPtr)
173
+
174
+ return { width: outputWidth, height: outputHeight, arr: output }
175
+ }
176
+
177
+ output = (data) => {
178
+ if (!data.isCompress) {
179
+ return {
180
+ width: data.width,
181
+ height: data.height,
182
+ arr: new Uint8ClampedArray(data.data)
183
+ }
184
+ }
185
+ return this.decompress(data)
186
+ }
187
+
188
+ getButtonCode (button) {
189
+ if (button === 0) {
190
+ return 1
191
+ } else if (button === 2) {
192
+ return 2
193
+ } else {
194
+ return 0
195
+ }
196
+ }
197
+
198
+ getKeyCode (event) {
199
+ const { type, button } = event
200
+ if (type.startsWith('key')) {
201
+ return scanCode(event)
202
+ } else if (type === 'mousemove') {
203
+ return 0
204
+ } else {
205
+ return this.getButtonCode(button)
206
+ }
207
+ }
208
+
209
+ handleCanvasEvent = e => {
210
+ // e.preventDefault()
211
+ // console.log('e', e)
212
+ const {
213
+ type,
214
+ clientX,
215
+ clientY
216
+ } = e
217
+ const {
218
+ left,
219
+ top
220
+ } = e.target.getBoundingClientRect()
221
+ const x = clientX - left
222
+ const y = clientY - top
223
+ // console.log('x,y', x, y, left, top, clientX, clientY, pageX, pageY)
224
+ const keyCode = this.getKeyCode(e)
225
+ const action = type.startsWith('key')
226
+ ? 'sendKeyEventScancode'
227
+ : type === 'mousewheel'
228
+ ? 'sendWheelEvent'
229
+ : 'sendPointerEvent'
230
+ const pressed = type === 'mousedown' || type === 'keydown'
231
+ let params = []
232
+ if (type.startsWith('mouse') || type.startsWith('context')) {
233
+ params = [x, y, keyCode, pressed]
234
+ } else if (type === 'wheel') {
235
+ const isHorizontal = false
236
+ const delta = isHorizontal ? e.deltaX : e.deltaY
237
+ const step = Math.round(Math.abs(delta) * 15 / 8)
238
+ console.log(x, y, step, delta, isHorizontal)
239
+ params = [x, y, step, delta > 0, isHorizontal]
240
+ } else if (type === 'keydown' || type === 'keyup') {
241
+ params = [keyCode, pressed]
242
+ }
243
+ this.socket.send(JSON.stringify({
244
+ action,
245
+ params
246
+ }))
247
+ }
248
+
249
+ onData = async (msg) => {
250
+ // console.log('msg', msg.data)
251
+ let { data } = msg
252
+ data = JSON.parse(data)
253
+ if (data.action === 'session-rdp-connected') {
254
+ return this.setState({
255
+ loading: false
256
+ })
257
+ }
258
+ const id = 'canvas_' + this.props.tab.id
259
+ const canvas = document.getElementById(id)
260
+ const ctx = canvas.getContext('2d')
261
+ const {
262
+ width,
263
+ height,
264
+ arr
265
+ } = this.output(data)
266
+ const imageData = ctx.createImageData(width, height)
267
+ imageData.data.set(arr)
268
+ ctx.putImageData(imageData, data.destLeft, data.destTop)
269
+ }
270
+
271
+ onerrorSocket = err => {
272
+ this.setStatus(statusMap.error)
273
+ log.error('socket error', err)
274
+ }
275
+
276
+ closeMsg = () => {
277
+ notification.destroy(this.warningKey)
278
+ }
279
+
280
+ handleClickClose = () => {
281
+ this.closeMsg()
282
+ this.handleReInit()
283
+ }
284
+
285
+ handleReInit = () => {
286
+ this.socket.send(JSON.stringify({
287
+ action: 'reload',
288
+ params: [
289
+ this.state.width,
290
+ this.state.height
291
+ ]
292
+ }))
293
+ this.setState({
294
+ loading: true
295
+ })
296
+ }
297
+
298
+ handleReload = () => {
299
+ this.closeMsg()
300
+ this.props.reloadTab(
301
+ this.props.tab
302
+ )
303
+ }
304
+
305
+ handleEditResolutions = () => {
306
+ window.store.toggleResolutionEdit()
307
+ }
308
+
309
+ oncloseSocket = () => {
310
+ // this.setStatus(
311
+ // statusMap.error
312
+ // )
313
+ // this.warningKey = `open${Date.now()}`
314
+ // notification.warning({
315
+ // key: this.warningKey,
316
+ // message: e('socketCloseTip'),
317
+ // duration: 30,
318
+ // description: (
319
+ // <div className='pd2y'>
320
+ // <Button
321
+ // className='mg1r'
322
+ // type='primary'
323
+ // onClick={this.handleClickClose}
324
+ // >
325
+ // {m('close')}
326
+ // </Button>
327
+ // <Button
328
+ // icon={<ReloadOutlined />}
329
+ // onClick={this.handleReload}
330
+ // >
331
+ // {m('reload')}
332
+ // </Button>
333
+ // </div>
334
+ // )
335
+ // })
336
+ }
337
+
338
+ getAllRes = () => {
339
+ return [
340
+ ...this.props.resolutions,
341
+ ...resolutions
342
+ ]
343
+ }
344
+
345
+ handleResChange = (v) => {
346
+ const res = this.getAllRes().find(d => d.id === v)
347
+ const id = `rdp-reso-${this.props.tab.host}`
348
+ ls.setItemJSON(id, res)
349
+ this.setState(res, this.handleReInit)
350
+ }
351
+
352
+ renderControl = () => {
353
+ const {
354
+ id
355
+ } = this.state
356
+ const sleProps = {
357
+ value: id,
358
+ onChange: this.handleResChange,
359
+ popupMatchSelectWidth: false
360
+ }
361
+ const {
362
+ host,
363
+ port,
364
+ username
365
+ } = this.props.tab
366
+ return (
367
+ <div className='pd1 fix'>
368
+ <ReloadOutlined
369
+ onClick={this.handleReInit}
370
+ className='mg2r mg1l pointer'
371
+ />
372
+ <Select
373
+ {...sleProps}
374
+ >
375
+ {
376
+ this.getAllRes().map(d => {
377
+ const v = d.id
378
+ return (
379
+ <Option
380
+ key={v}
381
+ value={v}
382
+ >
383
+ {d.width}x{d.height}
384
+ </Option>
385
+ )
386
+ })
387
+ }
388
+ </Select>
389
+ <EditOutlined
390
+ onClick={this.handleEditResolutions}
391
+ className='mg2r mg1l pointer'
392
+ />
393
+ <span className='mg2l mg2r'>
394
+ {username}@{host}:{port}
395
+ </span>
396
+ <HelpIcon
397
+ link={rdpHelpLink}
398
+ />
399
+ </div>
400
+ )
401
+ }
402
+
403
+ render () {
404
+ const { width: w, height: h } = this.computeProps()
405
+ const rdpProps = {
406
+ style: {
407
+ width: w + 'px',
408
+ height: h + 'px'
409
+ }
410
+ }
411
+ const { width, height, loading } = this.state
412
+ const canvasProps = {
413
+ width,
414
+ height,
415
+ onMouseDown: this.handleCanvasEvent,
416
+ onMouseUp: this.handleCanvasEvent,
417
+ onMouseMove: this.handleCanvasEvent,
418
+ onKeyDown: this.handleCanvasEvent,
419
+ onKeyUp: this.handleCanvasEvent,
420
+ onWheel: this.handleCanvasEvent,
421
+ onContextMenu: this.handleCanvasEvent,
422
+ tabIndex: 0
423
+ }
424
+ return (
425
+ <Spin spinning={loading}>
426
+ <div
427
+ {...rdpProps}
428
+ className='rdp-session-wrap pd1'
429
+ >
430
+ {this.renderControl()}
431
+ <canvas
432
+ {...canvasProps}
433
+ id={'canvas_' + this.props.tab.id}
434
+ />
435
+ </div>
436
+ </Spin>
437
+ )
438
+ }
439
+ }
@@ -0,0 +1,63 @@
1
+ import { Component } from '../common/react-subx'
2
+ import ResolutionForm from './resolution-form'
3
+ import {
4
+ Modal
5
+ } from 'antd'
6
+ import uid from '../../common/uid'
7
+
8
+ export default class Resolutions extends Component {
9
+ remove = id => {
10
+ const { store } = this.props
11
+ const { resolutions } = store
12
+ const index = resolutions.findIndex(d => d.id === id)
13
+ if (index < 0) {
14
+ return
15
+ }
16
+ resolutions.splice(index, 1)
17
+ store.setState('resolutions', resolutions)
18
+ }
19
+
20
+ submit = (data) => {
21
+ const { store } = this.props
22
+ const { resolutions } = store
23
+ resolutions.push({
24
+ ...data,
25
+ id: uid()
26
+ })
27
+ store.setState('resolutions', resolutions)
28
+ }
29
+
30
+ render () {
31
+ const {
32
+ openResolutionEdit,
33
+ toggleResolutionEdit,
34
+ resolutions
35
+ } = this.props.store
36
+ if (!openResolutionEdit) {
37
+ return null
38
+ }
39
+ const modalProps = {
40
+ footer: null,
41
+ visible: true,
42
+ onCancel: () => toggleResolutionEdit()
43
+ }
44
+ const resList = {
45
+ list: resolutions,
46
+ initialValues: {
47
+ width: 1600,
48
+ height: 900
49
+ },
50
+ remove: this.remove,
51
+ submit: this.submit
52
+ }
53
+ return (
54
+ <Modal
55
+ {...modalProps}
56
+ >
57
+ <ResolutionForm
58
+ {...resList}
59
+ />
60
+ </Modal>
61
+ )
62
+ }
63
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ Form,
3
+ InputNumber,
4
+ Space,
5
+ Button,
6
+ Table
7
+ } from 'antd'
8
+ import { MinusCircleFilled, CheckOutlined } from '@ant-design/icons'
9
+ import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
10
+
11
+ import resolutions from './resolutions'
12
+
13
+ const FormItem = Form.Item
14
+ const { prefix } = window
15
+ const e = prefix('ssh')
16
+ const c = prefix('common')
17
+ const m = prefix('menu')
18
+
19
+ export default function ResolutionForm (props) {
20
+ const {
21
+ list,
22
+ initialValues
23
+ } = props
24
+ const [formChild] = Form.useForm()
25
+ function handleFinish (data) {
26
+ props.submit(data)
27
+ formChild.resetFields()
28
+ }
29
+
30
+ function remove (id) {
31
+ props.remove(id)
32
+ }
33
+ const cols = [
34
+ {
35
+ title: 'NO.',
36
+ dataIndex: 'index',
37
+ key: 'index',
38
+ render: (k) => k
39
+ }, {
40
+ title: 'Resolutions',
41
+ key: 'customResolutions',
42
+ render: (k, item) => {
43
+ return `${item.width}x${item.height}`
44
+ }
45
+ }, {
46
+ title: m('del'),
47
+ key: 'op',
48
+ dataIndex: 'id',
49
+ render: (id, item) => {
50
+ if (item.readonly) {
51
+ return '-'
52
+ }
53
+ return (
54
+ <MinusCircleFilled
55
+ className='pointer'
56
+ onClick={() => remove(id)}
57
+ />
58
+ )
59
+ }
60
+ }
61
+ ]
62
+
63
+ function renderList () {
64
+ const all = [
65
+ ...list,
66
+ ...resolutions
67
+ ]
68
+ return (
69
+ <FormItem {...tailFormItemLayout}>
70
+ <Table
71
+ columns={cols}
72
+ className='mg3b'
73
+ pagination={false}
74
+ size='small'
75
+ dataSource={all.map((d, i) => {
76
+ return {
77
+ ...d,
78
+ index: i + 1
79
+ }
80
+ })}
81
+ />
82
+ </FormItem>
83
+ )
84
+ }
85
+
86
+ return (
87
+ <div className='pd3t'>
88
+ <Form
89
+ form={formChild}
90
+ onFinish={handleFinish}
91
+ initialValues={initialValues}
92
+ >
93
+ <FormItem
94
+ label={c('resolutions')}
95
+ {...formItemLayout}
96
+ required
97
+ className='ssh-tunnels-host'
98
+ >
99
+ <Space.Compact>
100
+ <FormItem
101
+ name='width'
102
+ label=''
103
+ required
104
+ >
105
+ <InputNumber
106
+ min={600}
107
+ max={8192}
108
+ placeholder={e('width')}
109
+ />
110
+ </FormItem>
111
+ <span className='pd1x'>x</span>
112
+ <FormItem
113
+ label=''
114
+ name='height'
115
+ required
116
+ >
117
+ <InputNumber
118
+ min={600}
119
+ max={8192}
120
+ placeholder={e('height')}
121
+ />
122
+ </FormItem>
123
+ <Button
124
+ htmlType='submit'
125
+ icon={<CheckOutlined />}
126
+ />
127
+ </Space.Compact>
128
+ </FormItem>
129
+ </Form>
130
+
131
+ {renderList()}
132
+ </div>
133
+ )
134
+ }