@electerm/electerm-react 1.72.48 → 1.80.3

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 (49) hide show
  1. package/client/common/constants.js +1 -1
  2. package/client/common/sftp.js +3 -1
  3. package/client/components/ai/ai-config.jsx +7 -1
  4. package/client/components/batch-op/batch-op.jsx +4 -5
  5. package/client/components/bg/css-overwrite.jsx +179 -0
  6. package/client/components/bg/shapes.js +501 -0
  7. package/client/components/bookmark-form/form-tabs.jsx +1 -0
  8. package/client/components/bookmark-form/local-form-ui.jsx +7 -1
  9. package/client/components/bookmark-form/render-bg.jsx +43 -0
  10. package/client/components/bookmark-form/serial-form-ui.jsx +7 -1
  11. package/client/components/bookmark-form/ssh-form-ui.jsx +14 -3
  12. package/client/components/bookmark-form/telnet-form-ui.jsx +7 -1
  13. package/client/components/bookmark-form/use-ui.jsx +68 -72
  14. package/client/components/common/ref.js +2 -0
  15. package/client/components/{sftp/confirm-modal-store.jsx → file-transfer/conflict-resolve.jsx} +86 -48
  16. package/client/components/file-transfer/transfer-queue.jsx +151 -0
  17. package/client/components/file-transfer/transfer.jsx +582 -0
  18. package/client/components/{sftp → file-transfer}/transports-action-store.jsx +35 -32
  19. package/client/components/{sftp → file-transfer}/transports-ui-store.jsx +2 -2
  20. package/client/components/main/main.jsx +25 -18
  21. package/client/components/main/wrapper.styl +16 -0
  22. package/client/components/profile/profile-list.jsx +1 -1
  23. package/client/components/quick-commands/qm.styl +4 -1
  24. package/client/components/quick-commands/quick-commands-list.jsx +16 -4
  25. package/client/components/setting-panel/list.jsx +1 -1
  26. package/client/components/setting-panel/setting-common.jsx +25 -23
  27. package/client/components/setting-panel/setting-terminal.jsx +2 -1
  28. package/client/components/setting-panel/terminal-bg-config.jsx +15 -2
  29. package/client/components/sftp/file-info-modal.jsx +3 -0
  30. package/client/components/sftp/file-item.jsx +25 -23
  31. package/client/components/sftp/file-read.js +1 -27
  32. package/client/components/sftp/list-table-ui.jsx +2 -2
  33. package/client/components/sidebar/transfer-history-modal.jsx +1 -1
  34. package/client/components/sidebar/transfer-list-control.jsx +1 -1
  35. package/client/components/sidebar/transport-ui.jsx +16 -9
  36. package/client/components/terminal/terminal.jsx +23 -1
  37. package/client/components/text-editor/simple-editor.jsx +164 -0
  38. package/client/components/text-editor/text-editor-form.jsx +6 -9
  39. package/client/css/includes/box.styl +2 -2
  40. package/client/store/tab.js +5 -1
  41. package/client/store/transfer-list.js +10 -51
  42. package/package.json +1 -1
  43. package/client/components/main/css-overwrite.jsx +0 -91
  44. package/client/components/sftp/transfer-conflict-store.jsx +0 -284
  45. package/client/components/sftp/transport-action-store.jsx +0 -422
  46. package/client/components/sftp/zip.js +0 -42
  47. /package/client/components/{main → bg}/custom-css.jsx +0 -0
  48. /package/client/components/{sftp → file-transfer}/transfer-speed-format.js +0 -0
  49. /package/client/components/{sftp → file-transfer}/transfer.styl +0 -0
@@ -0,0 +1,582 @@
1
+ import { Component } from 'react'
2
+ import copy from 'json-deep-copy'
3
+ import { isFunction } from 'lodash-es'
4
+ import generate from '../../common/uid'
5
+ import { typeMap, transferTypeMap, fileOperationsMap, fileActions } from '../../common/constants'
6
+ import fs from '../../common/fs'
7
+ import format, { computeLeftTime, computePassedTime } from './transfer-speed-format'
8
+ import {
9
+ getLocalFileInfo,
10
+ getRemoteFileInfo,
11
+ getFolderFromFilePath
12
+ } from '../sftp/file-read'
13
+ import resolve from '../../common/resolve'
14
+ import { refsTransfers, refsStatic, refs } from '../common/ref'
15
+ import './transfer.styl'
16
+
17
+ const { assign } = Object
18
+
19
+ export default class TransportAction extends Component {
20
+ constructor (props) {
21
+ super(props)
22
+ this.sessionId = props.transfer.sessionId
23
+ const {
24
+ id,
25
+ transferBatch = ''
26
+ } = props.transfer
27
+ this.id = `tr-${transferBatch}-${id}`
28
+ refsTransfers.add(this.id, this)
29
+ this.total = 0
30
+ this.transferred = 0
31
+ }
32
+
33
+ componentDidMount () {
34
+ if (this.props.inited) {
35
+ this.initTransfer()
36
+ }
37
+ }
38
+
39
+ componentDidUpdate (prevProps) {
40
+ if (
41
+ prevProps.inited !== this.props.inited &&
42
+ this.props.inited === true
43
+ ) {
44
+ this.initTransfer()
45
+ }
46
+ if (
47
+ this.props.pausing !== prevProps.pausing
48
+ ) {
49
+ if (this.props.pausing) {
50
+ this.pause()
51
+ } else {
52
+ this.resume()
53
+ }
54
+ }
55
+ }
56
+
57
+ componentWillUnmount () {
58
+ this.transport && this.transport.destroy()
59
+ this.transport = null
60
+ this.fromFile = null
61
+ refsTransfers.remove(this.id)
62
+ }
63
+
64
+ localCheckExist = (path) => {
65
+ return getLocalFileInfo(path).catch(console.log)
66
+ }
67
+
68
+ remoteCheckExist = (path, sessionId) => {
69
+ // return true
70
+ const sftp = refs.get('sftp-' + sessionId)?.sftp
71
+ if (!sftp) {
72
+ console.log('remoteCheckExist error', 'sftp not exist')
73
+ return false
74
+ }
75
+ return getRemoteFileInfo(sftp, path)
76
+ .then(r => r)
77
+ .catch((e) => {
78
+ console.log('remoteCheckExist error', e)
79
+ return false
80
+ })
81
+ }
82
+
83
+ checkExist = (type, path, sessionId) => {
84
+ return this[type + 'CheckExist'](path, sessionId)
85
+ }
86
+
87
+ update = (up) => {
88
+ const { id } = this.props.transfer
89
+ refsStatic.get('transfer-queue')?.addToQueue(
90
+ 'update',
91
+ id,
92
+ up
93
+ )
94
+ }
95
+
96
+ tagTransferError = (id, errorMsg) => {
97
+ // this.clear()
98
+ const { store } = window
99
+ const { fileTransfers } = store
100
+ const index = fileTransfers.findIndex(d => d.id === id)
101
+ if (index < 0) {
102
+ return
103
+ }
104
+
105
+ const tr = copy(fileTransfers[index])
106
+ assign(tr, {
107
+ host: tr.host,
108
+ error: errorMsg,
109
+ finishTime: Date.now()
110
+ })
111
+ store.addTransferHistory(tr)
112
+ refsStatic.get('transfer-queue')?.addToQueue(
113
+ 'delete',
114
+ id
115
+ )
116
+ }
117
+
118
+ // insert = (insts) => {
119
+ // const { fileTransfers } = window.store
120
+ // const { index } = this.props
121
+ // fileTransfers.splice(index, 1, ...insts)
122
+ // }
123
+
124
+ remoteList = () => {
125
+ window.store.remoteList(this.sessionId)
126
+ }
127
+
128
+ localList = () => {
129
+ window.store.localList(this.sessionId)
130
+ }
131
+
132
+ onEnd = (update = {}) => {
133
+ if (this.onCancel) {
134
+ return
135
+ }
136
+ const {
137
+ transfer,
138
+ config
139
+ } = this.props
140
+ const {
141
+ typeTo
142
+ } = transfer
143
+ const finishTime = Date.now()
144
+ if (!config.disableTransferHistory) {
145
+ const fromFile = transfer.fromFile || this.fromFile
146
+ const size = update.size || fromFile.size
147
+ const r = copy(transfer)
148
+ assign(r, {
149
+ finishTime,
150
+ startTime: this.startTime,
151
+ size,
152
+ next: null,
153
+ speed: format(size, this?.startTime)
154
+ })
155
+ window.store.addTransferHistory(
156
+ r
157
+ )
158
+ }
159
+ const cbs = [
160
+ this[typeTo + 'List']
161
+ ]
162
+ const cb = () => {
163
+ cbs.forEach(cb => cb())
164
+ }
165
+ this.cancel(cb)
166
+ }
167
+
168
+ onData = (transferred) => {
169
+ if (this.onCancel) {
170
+ return
171
+ }
172
+ const { transfer } = this.props
173
+ const up = {}
174
+ const total = transfer.fromFile.size
175
+ let percent = total === 0
176
+ ? 0
177
+ : Math.floor(100 * transferred / total)
178
+ percent = percent >= 100 ? 99 : percent
179
+ up.percent = percent
180
+ up.status = 'active'
181
+ up.transferred = transferred
182
+ up.startTime = this.startTime
183
+ up.speed = format(transferred, up.startTime)
184
+ assign(
185
+ up,
186
+ computeLeftTime(transferred, total, up.startTime)
187
+ )
188
+ up.passedTime = computePassedTime(up.startTime)
189
+ this.update(up)
190
+ }
191
+
192
+ cancel = (callback) => {
193
+ if (this.onCancel) {
194
+ return
195
+ }
196
+ this.onCancel = true
197
+ this.transport && this.transport.destroy()
198
+ this.transport = null
199
+ // window.store.cancelTransfer(this.props.transfer.id)
200
+ refsStatic.get('transfer-queue')?.addToQueue(
201
+ 'delete',
202
+ this.props.transfer.id
203
+ )
204
+ if (isFunction(callback)) {
205
+ callback()
206
+ }
207
+ }
208
+
209
+ pause = () => {
210
+ this.transport?.pause()
211
+ }
212
+
213
+ resume = () => {
214
+ this.transport?.resume()
215
+ }
216
+
217
+ mvOrCp = () => {
218
+ const {
219
+ transfer
220
+ } = this.props
221
+ const {
222
+ fromPath,
223
+ toPath,
224
+ typeFrom,
225
+ sessionId,
226
+ operation // 'mv' or 'cp'
227
+ } = transfer
228
+
229
+ let finalToPath = toPath
230
+
231
+ // Check if it's a copy operation to the same path
232
+ if (fromPath === toPath && operation === fileOperationsMap.cp) {
233
+ finalToPath = this.handleRename(toPath, typeFrom === typeMap.remote)
234
+ transfer.toPath = finalToPath
235
+ this.update({
236
+ toPath: finalToPath
237
+ })
238
+ }
239
+ if (typeFrom === typeMap.local) {
240
+ return fs[operation](fromPath, finalToPath)
241
+ .then(this.onEnd)
242
+ .catch(e => {
243
+ this.onEnd()
244
+ this.onError(e)
245
+ })
246
+ }
247
+ const sftp = refs.get('sftp-' + sessionId)?.sftp
248
+ return sftp[operation](fromPath, finalToPath)
249
+ .then(this.onEnd)
250
+ .catch(e => {
251
+ this.onEnd()
252
+ this.onError(e)
253
+ })
254
+ }
255
+
256
+ transferFile = async (transfer = this.props.transfer) => {
257
+ const {
258
+ fromPath,
259
+ typeFrom,
260
+ toFile = {}
261
+ } = transfer
262
+ const toPath = this.newPath || transfer.toPath
263
+ const fromFile = transfer.fromFile || this.fromFile
264
+ const fromMode = fromFile.mode
265
+ const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
266
+ const isDown = transferType === transferTypeMap.download
267
+ const localPath = isDown
268
+ ? toPath
269
+ : fromPath
270
+ const remotePath = isDown
271
+ ? fromPath
272
+ : toPath
273
+ const mode = toFile.mode || fromMode
274
+ const sftp = refs.get('sftp-' + this.sessionId).sftp
275
+ this.transport = await sftp[transferType]({
276
+ remotePath,
277
+ localPath,
278
+ options: { mode },
279
+ onData: this.onData,
280
+ onError: this.onError,
281
+ onEnd: this.onEnd
282
+ })
283
+ }
284
+
285
+ isTransferAction = (action) => {
286
+ return action.includes('rename') || action === 'transfer'
287
+ }
288
+
289
+ initTransfer = async () => {
290
+ if (this.started) {
291
+ return
292
+ }
293
+ this.started = true
294
+ const { transfer } = this.props
295
+ const {
296
+ id,
297
+ typeFrom,
298
+ typeTo,
299
+ fromPath,
300
+ toPath,
301
+ operation
302
+ } = transfer
303
+
304
+ if (
305
+ typeFrom === typeTo &&
306
+ fromPath === toPath &&
307
+ operation === fileOperationsMap.mv
308
+ ) {
309
+ return this.cancel()
310
+ }
311
+
312
+ const t = Date.now()
313
+ this.update({
314
+ startTime: t
315
+ })
316
+ this.startTime = t
317
+
318
+ const fromFile = transfer.fromFile
319
+ ? transfer.fromFile
320
+ : await this.checkExist(typeFrom, fromPath, this.sessionId)
321
+ if (!fromFile) {
322
+ return this.tagTransferError(id, 'file not exist')
323
+ }
324
+ this.fromFile = fromFile
325
+ this.update({
326
+ fromFile
327
+ })
328
+ if (fromPath === toPath && typeFrom === typeTo) {
329
+ return this.mvOrCp()
330
+ }
331
+ const hasConflict = await this.checkConflict()
332
+ if (hasConflict) {
333
+ return
334
+ }
335
+
336
+ if (typeFrom === typeTo) {
337
+ return this.mvOrCp()
338
+ }
339
+ this.startTransfer()
340
+ }
341
+
342
+ checkConflict = async (transfer = this.props.transfer) => {
343
+ const {
344
+ typeTo,
345
+ toPath,
346
+ sessionId
347
+ } = transfer
348
+ const transferStillExists = window.store.fileTransfers.some(t => t.id === transfer.id)
349
+ if (!transferStillExists) {
350
+ return false
351
+ }
352
+ const toFile = await this.checkExist(typeTo, toPath, sessionId)
353
+
354
+ if (toFile) {
355
+ this.update({
356
+ toFile
357
+ })
358
+ if (transfer.resolvePolicy) {
359
+ return this.onDecision(transfer.resolvePolicy)
360
+ }
361
+ if (this.resolvePolicy) {
362
+ return this.onDecision(this.resolvePolicy)
363
+ }
364
+ const transferWithToFile = {
365
+ ...copy(transfer),
366
+ toFile,
367
+ fromFile: copy(transfer.fromFile || this.fromFile)
368
+ }
369
+ refsStatic.get('transfer-conflict')?.addConflict(transferWithToFile)
370
+ return true
371
+ }
372
+ }
373
+
374
+ onDecision = (policy) => {
375
+ if (policy === fileActions.skip || policy === fileActions.cancel) {
376
+ return this.onEnd()
377
+ }
378
+
379
+ if (policy === fileActions.rename) {
380
+ const {
381
+ typeTo,
382
+ toPath
383
+ } = this.props.transfer
384
+ const newPath = this.handleRename(toPath, typeTo === typeMap.remote)
385
+ this.update({
386
+ toPath: newPath
387
+ })
388
+ this.newPath = newPath
389
+ }
390
+
391
+ this.startTransfer()
392
+ }
393
+
394
+ startTransfer = async () => {
395
+ const { fromFile = this.fromFile } = this.props.transfer
396
+
397
+ if (!fromFile.isDirectory) {
398
+ return this.transferFile()
399
+ }
400
+ await this.transferFolderRecursive()
401
+ this.onEnd({
402
+ transferred: this.transferred,
403
+ size: this.total
404
+ })
405
+ }
406
+
407
+ list = async (type, path, sessionId) => {
408
+ const sftp = refs.get('sftp-' + sessionId)
409
+ return sftp[type + 'List'](true, path)
410
+ }
411
+
412
+ handleRename = (fromPath, isRemote) => {
413
+ const { path, base, ext } = getFolderFromFilePath(fromPath, isRemote)
414
+ const newName = `${base}(rename-${generate()})${ext ? '.' + ext : ''}`
415
+ return resolve(path, newName)
416
+ }
417
+
418
+ onFolderData = (transferred) => {
419
+ if (this.onCancel) {
420
+ return
421
+ }
422
+ this.transferred += transferred
423
+ const up = {}
424
+ let percent = this.total === 0
425
+ ? 0
426
+ : Math.floor(100 * this.transferred / this.total)
427
+ percent = percent >= 100 ? 99 : percent
428
+ up.percent = percent
429
+ up.status = 'active'
430
+ up.transferred = this.transferred
431
+ up.startTime = this.startTime
432
+ up.speed = format(this.transferred, up.startTime)
433
+ assign(
434
+ up,
435
+ computeLeftTime(this.transferred, this.total, up.startTime)
436
+ )
437
+ up.passedTime = computePassedTime(up.startTime)
438
+ this.update(up)
439
+ }
440
+
441
+ transferFileAsSubTransfer = async (transfer) => {
442
+ const {
443
+ fromPath,
444
+ toPath,
445
+ typeFrom,
446
+ fromFile: {
447
+ mode: fromMode,
448
+ size: fileSize
449
+ },
450
+ toFile = {}
451
+ } = transfer
452
+
453
+ const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
454
+ const isDown = transferType === transferTypeMap.download
455
+ const localPath = isDown ? toPath : fromPath
456
+ const remotePath = isDown ? fromPath : toPath
457
+ const mode = toFile.mode || fromMode
458
+ const sftp = refs.get('sftp-' + this.sessionId).sftp
459
+
460
+ return new Promise((resolve, reject) => {
461
+ let transport
462
+
463
+ const onSubEnd = () => {
464
+ if (fileSize) {
465
+ this.onFolderData(fileSize)
466
+ }
467
+ if (transport) {
468
+ transport.destroy()
469
+ transport = null
470
+ }
471
+ resolve(fileSize)
472
+ }
473
+
474
+ const onSubError = (error) => {
475
+ if (transport) {
476
+ transport.destroy()
477
+ transport = null
478
+ }
479
+ reject(error)
480
+ }
481
+
482
+ sftp[transferType]({
483
+ remotePath,
484
+ localPath,
485
+ options: { mode },
486
+ onData: () => {},
487
+ onError: onSubError,
488
+ onEnd: onSubEnd
489
+ }).then(transportInstance => {
490
+ transport = transportInstance
491
+ }).catch(onSubError)
492
+ })
493
+ }
494
+
495
+ getDefaultTransfer = () => {
496
+ const transfer = this.props.transfer
497
+ if (this.newPath) {
498
+ const modifiedTransfer = {
499
+ ...transfer,
500
+ toPath: this.newPath,
501
+ isRenamed: true
502
+ }
503
+ return modifiedTransfer
504
+ }
505
+ return transfer
506
+ }
507
+
508
+ transferFolderRecursive = async (transfer = this.getDefaultTransfer()) => {
509
+ if (this.onCancel) {
510
+ return
511
+ }
512
+ const {
513
+ fromPath,
514
+ toPath,
515
+ typeFrom,
516
+ typeTo,
517
+ sessionId,
518
+ toFile,
519
+ isRenamed
520
+ } = transfer
521
+ if (!toFile || isRenamed) {
522
+ const folderCreated = await this.mkdir(transfer)
523
+ if (!folderCreated) {
524
+ return
525
+ }
526
+ }
527
+ const list = await this.list(typeFrom, fromPath, sessionId)
528
+
529
+ for (const item of list) {
530
+ if (!item.isDirectory) {
531
+ this.total += item.size
532
+ }
533
+ const fromItemPath = resolve(fromPath, item.name)
534
+ const toItemPath = resolve(toPath, item.name)
535
+
536
+ const itemTransfer = {
537
+ ...transfer,
538
+ fromPath: fromItemPath,
539
+ toPath: toItemPath,
540
+ fromFile: item
541
+ }
542
+
543
+ const toFile = await this.checkExist(typeTo, toItemPath, sessionId)
544
+ itemTransfer.toFile = toFile
545
+ if (item.isDirectory) {
546
+ await this.transferFolderRecursive(itemTransfer)
547
+ } else {
548
+ await this.transferFileAsSubTransfer(itemTransfer)
549
+ }
550
+ }
551
+ }
552
+
553
+ onError = (e) => {
554
+ const up = {
555
+ status: 'exception',
556
+ error: e.message
557
+ }
558
+ this.onEnd(up)
559
+ window.store.onError(e)
560
+ }
561
+
562
+ mkdir = async (transfer = this.props.transfer) => {
563
+ const {
564
+ typeTo,
565
+ toPath,
566
+ sessionId
567
+ } = transfer
568
+ if (typeTo === typeMap.local) {
569
+ return fs.mkdir(toPath)
570
+ .then(() => true)
571
+ .catch(() => false)
572
+ }
573
+ const sftp = refs.get('sftp-' + sessionId).sftp
574
+ return sftp.mkdir(toPath)
575
+ .then(() => true)
576
+ .catch(() => false)
577
+ }
578
+
579
+ render () {
580
+ return null
581
+ }
582
+ }
@@ -6,6 +6,8 @@
6
6
  import { Component } from 'react'
7
7
  import Transports from './transports-ui-store'
8
8
  import { maxTransport } from '../../common/constants'
9
+ import { refsStatic } from '../common/ref'
10
+ // import { action } from 'manate'
9
11
 
10
12
  export default class TransportsActionStore extends Component {
11
13
  componentDidMount () {
@@ -26,21 +28,26 @@ export default class TransportsActionStore extends Component {
26
28
  fileTransfers
27
29
  } = store
28
30
 
29
- fileTransfers.forEach(t => {
31
+ // First loop: Handle same type transfers
32
+ for (const t of fileTransfers) {
30
33
  const {
31
34
  typeTo,
32
35
  typeFrom,
33
- fromFile,
34
- inited
36
+ inited,
37
+ id
35
38
  } = t
36
- const ready = !!fromFile
37
- if (typeTo === typeFrom && ready && !inited) {
38
- t.inited = true
39
+ if (typeTo === typeFrom && !inited) {
40
+ refsStatic.get('transfer-queue')?.addToQueue(
41
+ 'update',
42
+ id,
43
+ {
44
+ inited: true
45
+ }
46
+ )
39
47
  }
40
- })
41
- // if (pauseAllTransfer) {
42
- // return store.setFileTransfers(fileTransfers)
43
- // }
48
+ }
49
+
50
+ // Count active transfers
44
51
  let count = fileTransfers.filter(t => {
45
52
  const {
46
53
  typeTo,
@@ -50,44 +57,40 @@ export default class TransportsActionStore extends Component {
50
57
  } = t
51
58
  return typeTo !== typeFrom && inited && pausing !== true
52
59
  }).length
60
+
53
61
  if (count >= maxTransport) {
54
62
  return
55
63
  }
64
+
65
+ // Second loop: Process pending transfers
56
66
  const len = fileTransfers.length
57
- // const ids = []
67
+
58
68
  for (let i = 0; i < len; i++) {
59
69
  const tr = fileTransfers[i]
60
70
  const {
61
71
  typeTo,
62
72
  typeFrom,
63
73
  inited,
64
- fromFile,
65
- action
74
+ id
66
75
  } = tr
67
- // if (!error) {
68
- // ids.push(id)
69
- // }
76
+
70
77
  const isTransfer = typeTo !== typeFrom
71
- const ready = (
72
- action && fromFile
73
- )
74
- if (
75
- !ready ||
76
- inited ||
77
- !isTransfer
78
- ) {
78
+
79
+ if (inited || !isTransfer) {
79
80
  continue
80
81
  }
81
- // if (isTransfer && tr.fromFile.isDirectory) {
82
- // i = len
83
- // continue
84
- // }
85
- if (
86
- fromFile && count < maxTransport
87
- ) {
82
+
83
+ if (count < maxTransport) {
88
84
  count++
89
- tr.inited = true
85
+ refsStatic.get('transfer-queue')?.addToQueue(
86
+ 'update',
87
+ id,
88
+ {
89
+ inited: true
90
+ }
91
+ )
90
92
  }
93
+
91
94
  if (count >= maxTransport) {
92
95
  break
93
96
  }
@@ -2,7 +2,7 @@
2
2
  * transporter UI component
3
3
  */
4
4
 
5
- import Transport from './transport-action-store'
5
+ import Transport from './transfer'
6
6
 
7
7
  export default function TransportsUI (props) {
8
8
  const { fileTransfers } = props
@@ -22,7 +22,7 @@ export default function TransportsUI (props) {
22
22
  return (
23
23
  <Transport
24
24
  {...trProps}
25
- key={id + ':tr:' + i}
25
+ key={id}
26
26
  />
27
27
  )
28
28
  })