@electerm/electerm-react 1.38.65 → 1.38.80

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 (43) hide show
  1. package/client/common/constants.js +6 -3
  2. package/client/common/create-title.jsx +9 -1
  3. package/client/common/sftp.js +3 -0
  4. package/client/components/batch-op/batch-op.jsx +1 -6
  5. package/client/components/bookmark-form/bookmark-form.styl +3 -1
  6. package/client/components/bookmark-form/index.jsx +12 -8
  7. package/client/components/bookmark-form/render-ssh-tunnel.jsx +210 -88
  8. package/client/components/bookmark-form/ssh-form-ui.jsx +1 -1
  9. package/client/components/bookmark-form/web-form-ui.jsx +96 -0
  10. package/client/components/bookmark-form/web-form.jsx +16 -0
  11. package/client/components/main/main.jsx +14 -0
  12. package/client/components/quick-commands/qm.styl +1 -1
  13. package/client/components/session/session.styl +4 -1
  14. package/client/components/session/sessions.jsx +16 -2
  15. package/client/components/session/web-session.jsx +20 -0
  16. package/client/components/sftp/{confirm-modal.jsx → confirm-modal-store.jsx} +81 -50
  17. package/client/components/sftp/file-item.jsx +2 -0
  18. package/client/components/sftp/sftp-entry.jsx +27 -37
  19. package/client/components/sftp/transfer-conflict-store.jsx +291 -0
  20. package/client/components/sftp/transport-action-store.jsx +430 -0
  21. package/client/components/sftp/transports-action-store.jsx +102 -0
  22. package/client/components/sftp/transports-ui-store.jsx +30 -0
  23. package/client/components/sidebar/transfer-list-control.jsx +5 -14
  24. package/client/components/sidebar/transport-ui.jsx +2 -12
  25. package/client/components/tabs/tab.jsx +43 -2
  26. package/client/components/tabs/tabs.styl +1 -1
  27. package/client/components/terminal/index.jsx +14 -1
  28. package/client/components/terminal/terminal-interactive.jsx +15 -0
  29. package/client/components/terminal-info/disk.jsx +9 -0
  30. package/client/entry/worker.js +5 -1
  31. package/client/store/index.js +16 -1
  32. package/client/store/init-state.js +2 -3
  33. package/client/store/sync.js +5 -2
  34. package/client/store/tab.js +1 -1
  35. package/client/store/transfer-list.js +55 -2
  36. package/client/store/watch.js +0 -8
  37. package/package.json +1 -1
  38. package/client/components/sftp/transfer-conflict.jsx +0 -323
  39. package/client/components/sftp/transport-action.jsx +0 -412
  40. package/client/components/sftp/transport-entry.jsx +0 -108
  41. package/client/components/sftp/transport-types.js +0 -8
  42. package/client/components/sftp/transports-action.jsx +0 -111
  43. package/client/components/sftp/transports-ui.jsx +0 -93
@@ -0,0 +1,291 @@
1
+ /**
2
+ * pass transfer list from props
3
+ * when list changes, do transfer and other op
4
+ */
5
+
6
+ import { Component } from '../common/react-subx'
7
+ import { typeMap } from '../../common/constants'
8
+ import {
9
+ getLocalFileInfo,
10
+ getRemoteFileInfo,
11
+ getFolderFromFilePath,
12
+ getFileExt,
13
+ checkFolderSize
14
+ } from './file-read'
15
+ import { findIndex, find } from 'lodash-es'
16
+ import generate from '../../common/uid'
17
+ import resolve from '../../common/resolve'
18
+
19
+ export default class TransferConflictStore extends Component {
20
+ state = {
21
+ currentId: ''
22
+ }
23
+
24
+ componentDidMount () {
25
+ this.watchFile()
26
+ }
27
+
28
+ componentDidUpdate (prevProps) {
29
+ if (
30
+ prevProps._fileTransfers !== this.props._fileTransfers
31
+ ) {
32
+ this.watchFile()
33
+ }
34
+ }
35
+
36
+ localCheckExist = (path) => {
37
+ return getLocalFileInfo(path)
38
+ }
39
+
40
+ remoteCheckExist = (path, sessionId) => {
41
+ const sftp = window.sftps[sessionId]
42
+ return getRemoteFileInfo(sftp, path)
43
+ .then(r => r)
44
+ .catch(() => false)
45
+ }
46
+
47
+ checkExist = (type, path, sessionId) => {
48
+ return this[type + 'CheckExist'](path, sessionId)
49
+ }
50
+
51
+ rename = (tr, action, _renameId) => {
52
+ const isRemote = tr.typeTo === typeMap.remote
53
+ const { path, name } = getFolderFromFilePath(tr.toPath, isRemote)
54
+ const { base, ext } = getFileExt(name)
55
+ const renameId = _renameId || generate()
56
+ const newName = ext
57
+ ? `${base}(rename-${renameId}).${ext}`
58
+ : `${base}(rename-${renameId})`
59
+ const res = {
60
+ ...tr,
61
+ renameId,
62
+ newName,
63
+ oldName: base,
64
+ toPath: resolve(path, newName)
65
+ }
66
+ if (action) {
67
+ res.action = action
68
+ }
69
+ return res
70
+ }
71
+
72
+ updateTransferAction = (data) => {
73
+ const {
74
+ id,
75
+ action,
76
+ transfer
77
+ } = data
78
+ const {
79
+ fromFile
80
+ } = transfer
81
+ this.clear()
82
+ const { store } = this.props
83
+ let {
84
+ fileTransfers
85
+ } = store
86
+ const index = findIndex(fileTransfers, d => d.id === id)
87
+ if (index < 0) {
88
+ return store.setFileTransfers(fileTransfers)
89
+ }
90
+ fileTransfers[index].fromFile = fromFile
91
+ fileTransfers[index].action = action
92
+ if (action === 'skip') {
93
+ fileTransfers.splice(index, 1)
94
+ } else if (action === 'cancel') {
95
+ fileTransfers = fileTransfers.slice(0, index)
96
+ }
97
+ if (action.includes('All')) {
98
+ fileTransfers = fileTransfers.map((t, i) => {
99
+ if (i < index) {
100
+ return t
101
+ }
102
+ return {
103
+ ...t,
104
+ action: action.replace('All', '')
105
+ }
106
+ })
107
+ }
108
+ if (action.includes('rename')) {
109
+ fileTransfers[index] = this.rename(fileTransfers[index])
110
+ } else if (action === 'skipAll') {
111
+ fileTransfers.splice(index, 1)
112
+ }
113
+ store.setFileTransfers(fileTransfers)
114
+ }
115
+
116
+ tagTransferError = (id, errorMsg) => {
117
+ const { store } = this.props
118
+ const {
119
+ fileTransfers
120
+ } = store
121
+ const tr = find(fileTransfers, d => d.id === id)
122
+ if (!tr) {
123
+ return
124
+ }
125
+ window.store.addTransferHistory({
126
+ ...tr,
127
+ host: tr.host,
128
+ error: errorMsg,
129
+ finishTime: Date.now()
130
+ })
131
+ const index = findIndex(fileTransfers, d => d.id === id)
132
+ if (index >= 0) {
133
+ fileTransfers.splice(index, 1)
134
+ }
135
+ store.setFileTransfers(fileTransfers)
136
+ }
137
+
138
+ setConflict (tr) {
139
+ if (window.store.transferToConfirm.id) {
140
+ return
141
+ }
142
+ window.store.setState(
143
+ 'transferToConfirm', tr
144
+ )
145
+ }
146
+
147
+ onDecision = (event) => {
148
+ if (
149
+ event &&
150
+ event.data &&
151
+ event.data.id === this.currentId
152
+ ) {
153
+ this.currentId = ''
154
+ this.updateTransferAction(event.data)
155
+ this.onConfirm = false
156
+ window.removeEventListener('message', this.onDecision)
157
+ }
158
+ }
159
+
160
+ waitForSignal = () => {
161
+ window.addEventListener('message', this.onDecision)
162
+ }
163
+
164
+ setCanTransfer = (fromFile, tr) => {
165
+ this.clear()
166
+ const {
167
+ store
168
+ } = this.props
169
+ const {
170
+ fileTransfers
171
+ } = store
172
+ const index = findIndex(fileTransfers, t => {
173
+ return t.id === tr.id
174
+ })
175
+ if (index >= 0) {
176
+ const up = {
177
+ action: 'transfer',
178
+ fromFile
179
+ }
180
+ Object.assign(fileTransfers[index], up)
181
+ } else {
182
+ fileTransfers[0].r = Math.random()
183
+ }
184
+ store.setFileTransfers(fileTransfers)
185
+ }
186
+
187
+ clear = () => {
188
+ this.currentId = ''
189
+ }
190
+
191
+ watchFile = async () => {
192
+ const { store } = this.props
193
+ const {
194
+ fileTransfers
195
+ } = store
196
+ if (!fileTransfers.length && this.currentId) {
197
+ return this.clear()
198
+ }
199
+ const tr = fileTransfers
200
+ .filter(t => {
201
+ return (
202
+ !t.action ||
203
+ !t.fromFile ||
204
+ t.fromFile.isDirectory
205
+ )
206
+ })[0]
207
+ if (!tr) {
208
+ this.onConfirm = false
209
+ return this.clear()
210
+ }
211
+ if (this.currentId) {
212
+ // fileTransfers[0].r = Math.random()
213
+ return store.setFileTransfers(fileTransfers)
214
+ }
215
+ this.currentId = tr.id
216
+ const {
217
+ typeFrom,
218
+ typeTo,
219
+ fromPath,
220
+ toPath,
221
+ id,
222
+ action,
223
+ renameId,
224
+ parentId,
225
+ skipConfirm,
226
+ sessionId
227
+ } = tr
228
+ const fromFile = tr.fromFile
229
+ ? tr.fromFile
230
+ : await this.checkExist(typeFrom, fromPath, sessionId)
231
+ if (!fromFile) {
232
+ this.currentId = ''
233
+ return this.tagTransferError(id, 'file not exist')
234
+ }
235
+ let toFile = false
236
+ if (renameId || parentId) {
237
+ toFile = false
238
+ } else if (fromPath === toPath && typeFrom === typeTo) {
239
+ toFile = true
240
+ } else {
241
+ toFile = await this.checkExist(typeTo, toPath, sessionId)
242
+ }
243
+ if (fromFile.isDirectory) {
244
+ const props = {
245
+ sftp: window.sftps[sessionId]
246
+ }
247
+ const skip = await checkFolderSize(props, fromFile)
248
+ .then(d => d && typeFrom !== typeTo)
249
+ if (!skip) {
250
+ return this.tagTransferError(id, 'folder too big or too many files in folder')
251
+ }
252
+ tr.zip = true
253
+ tr.skipExpand = true
254
+ }
255
+ if (fromPath === toPath && typeFrom === typeTo) {
256
+ return this.updateTransferAction({
257
+ id,
258
+ action: 'rename',
259
+ transfer: {
260
+ ...tr,
261
+ operation: 'cp',
262
+ fromFile
263
+ }
264
+ })
265
+ } else if (toFile && !action && !skipConfirm) {
266
+ this.waitForSignal(id)
267
+ if (!this.onConfirm) {
268
+ this.onConfirm = true
269
+ return this.setConflict({
270
+ ...tr,
271
+ fromFile,
272
+ toFile
273
+ })
274
+ }
275
+ } else if (toFile && !tr.fromFile && action) {
276
+ return this.updateTransferAction({
277
+ id,
278
+ action,
279
+ transfer: {
280
+ ...tr,
281
+ fromFile
282
+ }
283
+ })
284
+ }
285
+ this.setCanTransfer(fromFile, tr)
286
+ }
287
+
288
+ render () {
289
+ return null
290
+ }
291
+ }
@@ -0,0 +1,430 @@
1
+ import { Component } from '../common/react-subx'
2
+ import copy from 'json-deep-copy'
3
+ import { findIndex, isFunction } from 'lodash-es'
4
+ import generate from '../../common/uid'
5
+ import { typeMap, transferTypeMap, commonActions } from '../../common/constants'
6
+ import fs from '../../common/fs'
7
+ import format, { computeLeftTime, computePassedTime } from './transfer-speed-format'
8
+ import { getFolderFromFilePath } from './file-read'
9
+ import resolve from '../../common/resolve'
10
+ import delay from '../../common/wait'
11
+ import postMsg from '../../common/post-msg'
12
+ import { zipCmd, unzipCmd, rmCmd, mvCmd, mkdirCmd } from './zip'
13
+ import './transfer.styl'
14
+
15
+ export default class TransportAction extends Component {
16
+ constructor (props) {
17
+ super(props)
18
+ this.sessionId = props.transfer.sessionId
19
+ }
20
+
21
+ inst = {}
22
+ unzipping = false
23
+
24
+ componentDidMount () {
25
+ if (this.props.inited) {
26
+ this.initTransfer()
27
+ }
28
+ }
29
+
30
+ componentDidUpdate (prevProps) {
31
+ if (
32
+ prevProps.inited !== this.props.inited &&
33
+ this.props.inited === true
34
+ ) {
35
+ this.initTransfer()
36
+ }
37
+ if (
38
+ this.props.cancel === true &&
39
+ prevProps.cancel !== true
40
+ ) {
41
+ this.cancel()
42
+ }
43
+ if (
44
+ this.props.pause !== prevProps.pause
45
+ ) {
46
+ if (this.props.pause) {
47
+ this.pause()
48
+ } else {
49
+ this.resume()
50
+ }
51
+ }
52
+ }
53
+
54
+ update = (up) => {
55
+ const { store, transfer } = this.props
56
+ const {
57
+ fileTransfers
58
+ } = store
59
+ const index = findIndex(fileTransfers, t => t.id === transfer.id)
60
+ if (index < 0) {
61
+ return store.setFileTransfers(fileTransfers)
62
+ }
63
+ window.store.editTransfer(
64
+ fileTransfers[index].id,
65
+ up
66
+ )
67
+ Object.assign(fileTransfers[index], up)
68
+ store.setFileTransfers(fileTransfers)
69
+ }
70
+
71
+ insert = (insts) => {
72
+ const { store, transfer } = this.props
73
+ const {
74
+ fileTransfers
75
+ } = store
76
+ const index = findIndex(fileTransfers, t => t.id === transfer.id)
77
+ fileTransfers.splice(index, 1, ...insts)
78
+ store.setFileTransfers(fileTransfers)
79
+ }
80
+
81
+ remoteList = () => {
82
+ postMsg({
83
+ action: commonActions.sftpList,
84
+ sessionId: this.sessionId,
85
+ type: typeMap.remote
86
+ })
87
+ }
88
+
89
+ localList = () => {
90
+ postMsg({
91
+ action: commonActions.sftpList,
92
+ sessionId: this.sessionId,
93
+ type: typeMap.local
94
+ })
95
+ }
96
+
97
+ onEnd = (update = {}) => {
98
+ if (this.inst.onCancel) {
99
+ return
100
+ }
101
+ const {
102
+ transfer
103
+ } = this.props
104
+ const {
105
+ typeTo,
106
+ next
107
+ } = transfer
108
+ const cb = this[typeTo + 'List']
109
+ const finishTime = Date.now()
110
+ if (!this.props.store.config.disableTransferHistory) {
111
+ window.store.addTransferHistory(
112
+ {
113
+ ...transfer,
114
+ ...update,
115
+ finishTime,
116
+ startTime: this.inst.startTime,
117
+ size: transfer.fromFile.size,
118
+ next: null,
119
+ speed: format(transfer.fromFile.size, this.inst.startTime)
120
+ }
121
+ )
122
+ }
123
+ if (next) {
124
+ this.insert([copy(next)])
125
+ }
126
+ this.cancel(cb)
127
+ }
128
+
129
+ onData = (transferred) => {
130
+ if (this.inst.onCancel) {
131
+ return
132
+ }
133
+ const { transfer } = this.props
134
+ const up = {}
135
+ const total = transfer.fromFile.size
136
+ let percent = total === 0
137
+ ? 0
138
+ : Math.floor(100 * transferred / total)
139
+ percent = percent >= 100 ? 99 : percent
140
+ up.percent = percent
141
+ up.status = 'active'
142
+ up.transferred = transferred
143
+ up.startTime = this.inst.startTime
144
+ up.speed = format(transferred, up.startTime)
145
+ Object.assign(
146
+ up,
147
+ computeLeftTime(transferred, total, up.startTime)
148
+ )
149
+ up.passedTime = computePassedTime(up.startTime)
150
+ this.update(up)
151
+ }
152
+
153
+ cancel = (callback) => {
154
+ if (this.inst.onCancel) {
155
+ return
156
+ }
157
+ const {
158
+ transfer,
159
+ store
160
+ } = this.props
161
+ this.inst.onCancel = true
162
+ const { id } = transfer
163
+ this.inst.transport && this.inst.transport.destroy()
164
+ let {
165
+ fileTransfers
166
+ } = store
167
+ fileTransfers = fileTransfers.filter(t => {
168
+ return t.id !== id
169
+ })
170
+ store.setFileTransfers(fileTransfers)
171
+ if (isFunction(callback)) {
172
+ callback()
173
+ }
174
+ }
175
+
176
+ pause = () => {
177
+ this.inst.transport && this.inst.transport.pause()
178
+ }
179
+
180
+ resume = () => {
181
+ this.inst.transport && this.inst.transport.resume()
182
+ }
183
+
184
+ mvOrCp = () => {
185
+ const {
186
+ transfer
187
+ } = this.props
188
+ const {
189
+ fromPath,
190
+ toPath,
191
+ typeFrom,
192
+ sessionId,
193
+ operation // 'mv' or 'cp'
194
+ } = transfer
195
+ if (typeFrom === typeMap.local) {
196
+ return fs[operation](fromPath, toPath)
197
+ .then(this.onEnd)
198
+ .catch(e => {
199
+ this.onEnd()
200
+ this.onError(e)
201
+ })
202
+ }
203
+ const sftp = window.sftps[sessionId]
204
+ return sftp[operation](fromPath, toPath)
205
+ .then(this.onEnd)
206
+ .catch(e => {
207
+ this.onEnd()
208
+ this.onError(e)
209
+ })
210
+ }
211
+
212
+ zipTransfer = async () => {
213
+ const {
214
+ transfer
215
+ } = this.props
216
+ const {
217
+ fromPath,
218
+ toPath,
219
+ typeFrom,
220
+ sessionId
221
+ } = transfer
222
+ let p
223
+ let isFromRemote
224
+ if (typeFrom === typeMap.local) {
225
+ isFromRemote = false
226
+ p = await fs.zipFolder(fromPath)
227
+ } else {
228
+ isFromRemote = true
229
+ p = await zipCmd('', sessionId, fromPath)
230
+ }
231
+ const { name } = getFolderFromFilePath(p, isFromRemote)
232
+ const { path } = getFolderFromFilePath(toPath, !isFromRemote)
233
+ const nTo = resolve(path, name)
234
+ const newTrans1 = {
235
+ ...copy(transfer),
236
+ toPathReal: transfer.toPath,
237
+ fromPathReal: transfer.fromPath,
238
+ toPath: nTo,
239
+ fromPath: p,
240
+ originalId: transfer.id,
241
+ id: generate()
242
+ }
243
+ delete newTrans1.fromFile
244
+ delete newTrans1.inited
245
+ delete newTrans1.zip
246
+ const newTrans2 = copy(newTrans1)
247
+ newTrans2.unzip = true
248
+ newTrans2.id = generate()
249
+ newTrans1.next = newTrans2
250
+ this.insert([newTrans1])
251
+ }
252
+
253
+ buildUnzipPath = (transfer) => {
254
+ const {
255
+ newName,
256
+ toPath,
257
+ typeTo,
258
+ oldName
259
+ } = transfer
260
+ const isToRemote = typeTo === typeMap.remote
261
+ const { path } = getFolderFromFilePath(toPath, isToRemote)
262
+ const np = newName
263
+ ? resolve(path, 'temp-' + newName)
264
+ : path
265
+ return {
266
+ targetPath: path,
267
+ path: np,
268
+ name: oldName
269
+ }
270
+ }
271
+
272
+ unzipFile = async () => {
273
+ if (this.unzipping) {
274
+ return false
275
+ }
276
+ this.unzipping = true
277
+ const { transfer } = this.props
278
+ const {
279
+ fromPath,
280
+ toPath,
281
+ typeTo,
282
+ newName,
283
+ sessionId
284
+ } = transfer
285
+ const isToRemote = typeTo === typeMap.remote
286
+ const {
287
+ path,
288
+ name,
289
+ targetPath
290
+ } = this.buildUnzipPath(transfer)
291
+ if (isToRemote) {
292
+ if (newName) {
293
+ await mkdirCmd('', sessionId, path)
294
+ await delay(1000)
295
+ }
296
+ await unzipCmd('', sessionId, toPath, path)
297
+ if (newName) {
298
+ const mvFrom = resolve(path, name)
299
+ const mvTo = resolve(targetPath, newName)
300
+ await mvCmd('', sessionId, mvFrom, mvTo)
301
+ }
302
+ } else {
303
+ if (newName) {
304
+ await fs.mkdir(path)
305
+ }
306
+ await fs.unzipFile(toPath, path)
307
+ if (newName) {
308
+ const mvFrom = resolve(path, name)
309
+ const mvTo = resolve(targetPath, newName)
310
+ await fs.mv(mvFrom, mvTo)
311
+ }
312
+ }
313
+ await rmCmd('', sessionId, !isToRemote ? fromPath : toPath)
314
+ await fs.rmrf(!isToRemote ? toPath : fromPath)
315
+ if (newName) {
316
+ if (isToRemote) {
317
+ await rmCmd('', sessionId, path)
318
+ } else {
319
+ await fs.rmrf(path)
320
+ }
321
+ }
322
+ this.onEnd()
323
+ }
324
+
325
+ doTransfer = async () => {
326
+ const { transfer } = this.props
327
+ const {
328
+ fromPath,
329
+ toPath,
330
+ typeFrom,
331
+ fromFile: {
332
+ mode: fromMode
333
+ },
334
+ toFile = {}
335
+ } = transfer
336
+ const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
337
+ const isDown = transferType === transferTypeMap.download
338
+ const localPath = isDown
339
+ ? toPath
340
+ : fromPath
341
+ const remotePath = isDown
342
+ ? fromPath
343
+ : toPath
344
+ const mode = toFile.mode || fromMode
345
+ const sftp = window.sftps[this.sessionId]
346
+ this.inst.transport = await sftp[transferType]({
347
+ remotePath,
348
+ localPath,
349
+ options: { mode },
350
+ onData: this.onData,
351
+ onError: this.onError,
352
+ onEnd: this.onEnd
353
+ })
354
+ }
355
+
356
+ isTransferAction = (action) => {
357
+ return action.includes('rename') || action === 'transfer'
358
+ }
359
+
360
+ initTransfer = async () => {
361
+ if (this.inst.started) {
362
+ return
363
+ }
364
+ const { transfer } = this.props
365
+ const {
366
+ typeFrom,
367
+ typeTo,
368
+ fromFile: {
369
+ isDirectory
370
+ },
371
+ action,
372
+ expanded,
373
+ zip,
374
+ unzip,
375
+ inited
376
+ } = transfer
377
+ const t = Date.now()
378
+ this.update({
379
+ startTime: t
380
+ })
381
+ this.inst.startTime = t
382
+ this.inst.started = true
383
+ if (unzip && inited) {
384
+ this.unzipFile()
385
+ } else if (zip && inited) {
386
+ this.zipTransfer()
387
+ } else if (typeFrom === typeTo) {
388
+ return this.mvOrCp()
389
+ } else if (isDirectory && expanded && this.isTransferAction(action)) {
390
+ return this.mkdir()
391
+ .then(this.onEnd)
392
+ .catch(this.onError)
393
+ } else if (!isDirectory) {
394
+ this.doTransfer()
395
+ } else if (expanded && isDirectory && !this.isTransferAction(action)) {
396
+ this.cancel()
397
+ }
398
+ }
399
+
400
+ onError = (e) => {
401
+ const up = {
402
+ status: 'exception',
403
+ error: e.message
404
+ }
405
+ this.onEnd(up)
406
+ window.store.onError(e)
407
+ }
408
+
409
+ mkdir = async () => {
410
+ const {
411
+ transfer
412
+ } = this.props
413
+ const {
414
+ typeTo,
415
+ toPath,
416
+ sessionId
417
+ } = transfer
418
+ if (typeTo === typeMap.local) {
419
+ return fs.mkdir(toPath)
420
+ .catch(this.onError)
421
+ }
422
+ const sftp = window.sftps[sessionId]
423
+ return sftp.mkdir(toPath)
424
+ .catch(this.onError)
425
+ }
426
+
427
+ render () {
428
+ return null
429
+ }
430
+ }