@electerm/electerm-react 1.38.65 → 1.38.70

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 (35) hide show
  1. package/client/common/constants.js +3 -2
  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/render-ssh-tunnel.jsx +210 -88
  7. package/client/components/bookmark-form/ssh-form-ui.jsx +1 -1
  8. package/client/components/main/main.jsx +14 -0
  9. package/client/components/sftp/{confirm-modal.jsx → confirm-modal-store.jsx} +81 -50
  10. package/client/components/sftp/file-item.jsx +2 -0
  11. package/client/components/sftp/sftp-entry.jsx +27 -37
  12. package/client/components/sftp/transfer-conflict-store.jsx +291 -0
  13. package/client/components/sftp/transport-action-store.jsx +430 -0
  14. package/client/components/sftp/transports-action-store.jsx +102 -0
  15. package/client/components/sftp/transports-ui-store.jsx +30 -0
  16. package/client/components/sidebar/transfer-list-control.jsx +5 -14
  17. package/client/components/sidebar/transport-ui.jsx +2 -12
  18. package/client/components/tabs/tab.jsx +43 -2
  19. package/client/components/tabs/tabs.styl +1 -1
  20. package/client/components/terminal/index.jsx +1 -0
  21. package/client/components/terminal/terminal-interactive.jsx +15 -0
  22. package/client/components/terminal-info/disk.jsx +9 -0
  23. package/client/store/index.js +4 -0
  24. package/client/store/init-state.js +2 -3
  25. package/client/store/sync.js +5 -2
  26. package/client/store/tab.js +1 -1
  27. package/client/store/transfer-list.js +55 -2
  28. package/client/store/watch.js +0 -8
  29. package/package.json +1 -1
  30. package/client/components/sftp/transfer-conflict.jsx +0 -323
  31. package/client/components/sftp/transport-action.jsx +0 -412
  32. package/client/components/sftp/transport-entry.jsx +0 -108
  33. package/client/components/sftp/transport-types.js +0 -8
  34. package/client/components/sftp/transports-action.jsx +0 -111
  35. package/client/components/sftp/transports-ui.jsx +0 -93
@@ -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
+ }
@@ -0,0 +1,102 @@
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 Transports from './transports-ui-store'
8
+ import { maxTransport } from '../../common/constants'
9
+
10
+ export default class TransportsActionStore extends Component {
11
+ componentDidMount () {
12
+ this.control()
13
+ }
14
+
15
+ componentDidUpdate (prevProps) {
16
+ if (
17
+ prevProps._fileTransfers !== this.props._fileTransfers
18
+ ) {
19
+ this.control()
20
+ }
21
+ }
22
+
23
+ control = async () => {
24
+ const { store } = this.props
25
+ let {
26
+ fileTransfers
27
+ } = store
28
+
29
+ fileTransfers = fileTransfers.map(t => {
30
+ const {
31
+ typeTo,
32
+ typeFrom,
33
+ fromFile,
34
+ inited
35
+ } = t
36
+ const ready = !!fromFile
37
+ if (typeTo === typeFrom && ready && !inited) {
38
+ t.inited = true
39
+ }
40
+ return t
41
+ })
42
+ // if (pauseAllTransfer) {
43
+ // return store.setFileTransfers(fileTransfers)
44
+ // }
45
+ let count = fileTransfers.filter(t => {
46
+ const {
47
+ typeTo,
48
+ typeFrom,
49
+ inited
50
+ } = t
51
+ return typeTo !== typeFrom && inited
52
+ }).length
53
+ if (count >= maxTransport) {
54
+ return store.setFileTransfers(fileTransfers)
55
+ }
56
+ const len = fileTransfers.length
57
+ const ids = []
58
+ for (let i = 0; i < len; i++) {
59
+ const tr = fileTransfers[i]
60
+ const {
61
+ typeTo,
62
+ typeFrom,
63
+ inited,
64
+ fromFile,
65
+ error,
66
+ id,
67
+ action
68
+ } = tr
69
+ if (!error) {
70
+ ids.push(id)
71
+ }
72
+ const isTransfer = typeTo !== typeFrom
73
+ const ready = (
74
+ action && fromFile
75
+ )
76
+ if (
77
+ !ready ||
78
+ inited ||
79
+ !isTransfer
80
+ ) {
81
+ continue
82
+ }
83
+ // if (isTransfer && tr.fromFile.isDirectory) {
84
+ // i = len
85
+ // continue
86
+ // }
87
+ if (
88
+ fromFile && count < maxTransport
89
+ ) {
90
+ count++
91
+ tr.inited = true
92
+ }
93
+ }
94
+ store.setFileTransfers(fileTransfers)
95
+ }
96
+
97
+ render () {
98
+ return (
99
+ <Transports {...this.props} />
100
+ )
101
+ }
102
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * transporter UI component
3
+ */
4
+ import { Component } from '../common/react-subx'
5
+ import Transport from './transport-action-store'
6
+
7
+ export default class TransportsUI extends Component {
8
+ render () {
9
+ const { store } = this.props
10
+ const {
11
+ fileTransfers
12
+ } = store
13
+ if (!fileTransfers.length) {
14
+ return null
15
+ }
16
+ return fileTransfers.map((t, i) => {
17
+ const { id } = t
18
+ return (
19
+ <Transport
20
+ {...this.props}
21
+ transfer={t}
22
+ inited={t.inited}
23
+ cancel={t.cancel}
24
+ pause={t.pausing}
25
+ key={id + ':tr:' + i}
26
+ />
27
+ )
28
+ })
29
+ }
30
+ }
@@ -8,10 +8,6 @@ import {
8
8
  PauseCircleOutlined
9
9
  } from '@ant-design/icons'
10
10
  import { get } from 'lodash-es'
11
- import {
12
- transportTypes
13
- } from '../sftp/transport-types'
14
- import postMessage from '../../common/post-msg'
15
11
 
16
12
  const { Option } = Select
17
13
 
@@ -28,21 +24,16 @@ export default class TransferModalUI extends Component {
28
24
  }
29
25
 
30
26
  handlePauseOrResumeAll = () => {
31
- postMessage({
32
- action: transportTypes.pauseOrResumeAll,
33
- id: this.state.filter
34
- })
27
+ const { store } = window
28
+ store.pauseAllTransfer ? store.resumeAll() : store.pauseAll()
35
29
  }
36
30
 
37
31
  handleCancelAll = () => {
38
- postMessage({
39
- action: transportTypes.cancelAll,
40
- id: this.state.filter
41
- })
32
+ window.store.cancelAll()
42
33
  }
43
34
 
44
35
  getGroups = () => {
45
- const fileTransfers = this.props.store.getTransfers()
36
+ const fileTransfers = this.props.store.fileTransfers
46
37
  const tree = fileTransfers.reduce((p, k) => {
47
38
  const {
48
39
  id,
@@ -79,7 +70,7 @@ export default class TransferModalUI extends Component {
79
70
  const {
80
71
  filter
81
72
  } = this.state
82
- const fileTransfers = this.props.store.getTransfers()
73
+ const fileTransfers = this.props.store.fileTransfers
83
74
  return filter === 'all'
84
75
  ? fileTransfers
85
76
  : fileTransfers.filter(d => d.sessionId === filter)
@@ -8,10 +8,6 @@ import {
8
8
  PlayCircleOutlined,
9
9
  PauseCircleOutlined
10
10
  } from '@ant-design/icons'
11
- import {
12
- transportTypes
13
- } from '../sftp/transport-types'
14
- import postMessage from '../../common/post-msg'
15
11
  import './transfer.styl'
16
12
 
17
13
  const { prefix } = window
@@ -35,16 +31,10 @@ export default function Transporter (props) {
35
31
  id
36
32
  } = props.transfer
37
33
  function cancel () {
38
- postMessage({
39
- action: transportTypes.cancelTransport,
40
- id
41
- })
34
+ window.store.cancelTransfer(id)
42
35
  }
43
36
  function handlePauseOrResume () {
44
- postMessage({
45
- action: transportTypes.pauseOrResumeTransfer,
46
- id
47
- })
37
+ window.store.toggleTransfer(id)
48
38
  }
49
39
  const isTransfer = typeTo !== typeFrom
50
40
  const Icon = !pausing ? PauseCircleOutlined : PlayCircleOutlined
@@ -26,6 +26,7 @@ import { shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
26
26
  const { prefix } = window
27
27
  const e = prefix('tabs')
28
28
  const m = prefix('menu')
29
+ const s = prefix('sftp')
29
30
  const onDragCls = 'ondrag-tab'
30
31
  const onDragOverCls = 'dragover-tab'
31
32
 
@@ -346,6 +347,36 @@ class Tab extends Component {
346
347
  )
347
348
  }
348
349
 
350
+ // sshTunnelResults is a array of { sshTunnel, error? }, sshTunnel is a object has props of sshTunnelLocalPort, sshTunnelRemoteHost, sshTunnelRemotePort, sshTunnel, sshTunnelLocalHost, should build sshTunnel string from sshTunnel object, when error exist, this string should start with "error:", return title and sshTunnelResults list in react element.
351
+ renderTitle = (sshTunnelResults, title) => {
352
+ const list = sshTunnelResults.map(({ sshTunnel: obj, error }, i) => {
353
+ const {
354
+ sshTunnelLocalPort,
355
+ sshTunnelRemoteHost = '127.0.0.1',
356
+ sshTunnelRemotePort,
357
+ sshTunnel,
358
+ sshTunnelLocalHost = '127.0.0.1',
359
+ name
360
+ } = obj
361
+ let tunnel = sshTunnel === 'forwardRemoteToLocal'
362
+ ? `-> ${s('remote')}:${sshTunnelRemoteHost}:${sshTunnelRemotePort} -> ${sshTunnelLocalHost}:${sshTunnelLocalPort}`
363
+ : `-> ${s('local')}:${sshTunnelLocalHost}:${sshTunnelLocalPort} -> ${sshTunnelRemoteHost}:${sshTunnelRemotePort}`
364
+ if (error) {
365
+ tunnel = `error: ${tunnel}`
366
+ }
367
+ if (name) {
368
+ tunnel = `[${name}] ${tunnel}`
369
+ }
370
+ return <div key={tunnel}>{tunnel}</div>
371
+ })
372
+ return (
373
+ <div>
374
+ <div>${title}</div>
375
+ {list}
376
+ </div>
377
+ )
378
+ }
379
+
349
380
  renderCloseIcon () {
350
381
  return (
351
382
  <span className='tab-close pointer'>
@@ -360,7 +391,13 @@ class Tab extends Component {
360
391
  } = this.props
361
392
  const { isLast } = this.props
362
393
  const { tab, terminalOnData } = this.state
363
- const { id, isEditting, status, isTransporting } = tab
394
+ const {
395
+ id,
396
+ isEditting,
397
+ status,
398
+ isTransporting,
399
+ sshTunnelResults
400
+ } = tab
364
401
  const active = id === currentTabId
365
402
  const cls = classnames(
366
403
  `tab-${id}`,
@@ -378,6 +415,10 @@ class Tab extends Component {
378
415
  }
379
416
  )
380
417
  const title = createName(tab)
418
+ let tooltipTitle = title
419
+ if (sshTunnelResults) {
420
+ tooltipTitle = this.renderTitle(sshTunnelResults, title)
421
+ }
381
422
  if (isEditting) {
382
423
  return this.renderEditting(tab, cls)
383
424
  }
@@ -387,7 +428,7 @@ class Tab extends Component {
387
428
  : {}
388
429
  return (
389
430
  <Tooltip
390
- title={title}
431
+ title={tooltipTitle}
391
432
  placement='top'
392
433
  >
393
434
  <div