@electerm/electerm-react 1.38.86 → 1.39.2

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