@electerm/electerm-react 2.13.6 → 2.16.6

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 (59) hide show
  1. package/client/components/ai/ai-chat.jsx +44 -2
  2. package/client/components/ai/ai-stop-icon.jsx +13 -0
  3. package/client/components/ai/ai.styl +10 -0
  4. package/client/components/bg/css-overwrite.jsx +158 -187
  5. package/client/components/bg/custom-css.jsx +9 -15
  6. package/client/components/bookmark-form/bookmark-schema.js +7 -1
  7. package/client/components/bookmark-form/common/color-picker.jsx +4 -8
  8. package/client/components/bookmark-form/common/exec-settings-field.jsx +44 -0
  9. package/client/components/bookmark-form/common/fields.jsx +3 -0
  10. package/client/components/bookmark-form/config/common-fields.js +1 -0
  11. package/client/components/bookmark-form/config/local.js +3 -1
  12. package/client/components/common/animate-text.jsx +22 -23
  13. package/client/components/common/modal.jsx +2 -0
  14. package/client/components/common/notification.jsx +1 -1
  15. package/client/components/common/opacity.jsx +8 -6
  16. package/client/components/common/password.jsx +19 -32
  17. package/client/components/footer/cmd-history.jsx +154 -0
  18. package/client/components/footer/cmd-history.styl +73 -0
  19. package/client/components/footer/footer-entry.jsx +15 -1
  20. package/client/components/main/main.jsx +2 -3
  21. package/client/components/main/ui-theme.jsx +10 -6
  22. package/client/components/profile/profile-list.jsx +1 -1
  23. package/client/components/quick-commands/quick-commands-list.jsx +1 -1
  24. package/client/components/quick-commands/quick-commands-select.jsx +1 -4
  25. package/client/components/rdp/rdp-session.jsx +23 -4
  26. package/client/components/session/session.styl +1 -3
  27. package/client/components/setting-panel/list.styl +7 -0
  28. package/client/components/setting-panel/terminal-bg-config.jsx +2 -0
  29. package/client/components/setting-panel/text-bg-modal.jsx +9 -9
  30. package/client/components/setting-sync/setting-sync-form.jsx +10 -5
  31. package/client/components/sftp/file-item.jsx +22 -0
  32. package/client/components/sidebar/history-item.jsx +6 -3
  33. package/client/components/sidebar/history.jsx +48 -5
  34. package/client/components/sidebar/sidebar-panel.jsx +0 -13
  35. package/client/components/sidebar/sidebar.styl +19 -0
  36. package/client/components/ssh-config/load-ssh-configs-item.jsx +99 -0
  37. package/client/components/ssh-config/load-ssh-configs.jsx +38 -9
  38. package/client/components/ssh-config/ssh-config.styl +3 -0
  39. package/client/components/tabs/add-btn-menu.jsx +28 -4
  40. package/client/components/tabs/add-btn.jsx +1 -1
  41. package/client/components/tabs/add-btn.styl +8 -0
  42. package/client/components/terminal/terminal.jsx +28 -11
  43. package/client/components/terminal/transfer-client-base.js +44 -0
  44. package/client/components/terminal/trzsz-client.js +10 -11
  45. package/client/components/terminal/zmodem-client.js +10 -11
  46. package/client/components/text-editor/edit-with-custom-editor.jsx +49 -0
  47. package/client/components/text-editor/simple-editor.jsx +38 -6
  48. package/client/components/text-editor/text-editor-form.jsx +13 -5
  49. package/client/components/text-editor/text-editor.jsx +20 -1
  50. package/client/components/vnc/vnc-session.jsx +3 -0
  51. package/client/components/vnc/vnc.styl +1 -1
  52. package/client/store/bookmark.js +3 -11
  53. package/client/store/common.js +31 -4
  54. package/client/store/init-state.js +26 -1
  55. package/client/store/store.js +1 -1
  56. package/client/store/sync.js +2 -3
  57. package/client/store/watch.js +8 -1
  58. package/package.json +1 -1
  59. package/client/components/ssh-config/ssh-config-item.jsx +0 -24
@@ -18,6 +18,7 @@ export class TransferClientBase {
18
18
  this.currentTransfer = null
19
19
  this.savePath = null
20
20
  this.messageHandler = null
21
+ this._prevProgressRows = 0
21
22
  }
22
23
 
23
24
  /**
@@ -107,6 +108,49 @@ export class TransferClientBase {
107
108
  this.writeToTerminal(`\x1b[32m\x1b[1m${this.getProtocolDisplayName()}::${type}::START\x1b[0m\r\n`)
108
109
  }
109
110
 
111
+ /**
112
+ * Write progress bar to terminal
113
+ * @param {Object} options - Progress options
114
+ * @param {string} options.name - File name
115
+ * @param {number} options.size - Total size in bytes
116
+ * @param {number} options.transferred - Transferred bytes
117
+ * @param {number} options.speed - Transfer speed in bytes/s
118
+ * @param {boolean} options.isComplete - Whether transfer is complete
119
+ * @param {Function} options.formatSize - Function to format size
120
+ * @returns {string} The progress string written to terminal
121
+ */
122
+ writeProgressBar ({ name, size, transferred, speed, isComplete = false, formatSize = (b) => b }) {
123
+ const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
124
+ const barWidth = 30
125
+ const filledWidth = Math.floor(percent / 100 * barWidth)
126
+ const emptyWidth = barWidth - filledWidth
127
+
128
+ const bar = '\x1b[32m' + '\u2588'.repeat(filledWidth) + '\x1b[90m' + '\u2591'.repeat(emptyWidth) + '\x1b[0m'
129
+
130
+ const sizeStr = `${formatSize(transferred)}/${formatSize(size)}`
131
+ const speedStr = speed > 0 ? `, ${formatSize(speed)}/s` : ''
132
+ const doneStr = isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''
133
+
134
+ const str = `\x1b[32m${name}\x1b[0m: ${percent}% ${bar} ${sizeStr}${speedStr}${doneStr}`
135
+
136
+ // Calculate visible length (no ANSI codes) to detect line wrapping
137
+ const visibleLen = name.length + 2 + String(percent).length + 2 + barWidth + 1 + sizeStr.length + speedStr.length + (isComplete ? 7 : 0)
138
+ const cols = this.terminal?.term?.cols || 80
139
+ const currentRows = Math.max(1, Math.ceil(visibleLen / cols))
140
+
141
+ // Move cursor back up to the start of the previous progress block, then
142
+ // erase everything from there to end-of-display so wrapped lines are gone.
143
+ let clearSeq = '\r'
144
+ for (let i = 0; i < this._prevProgressRows; i++) {
145
+ clearSeq += '\x1b[A' // cursor up one row
146
+ }
147
+ clearSeq += '\x1b[J' // erase from cursor to end of display
148
+
149
+ this._prevProgressRows = currentRows - 1
150
+ this.writeToTerminal(clearSeq + str + '\r')
151
+ return str
152
+ }
153
+
110
154
  /**
111
155
  * Get protocol display name
112
156
  * Should be overridden by subclass
@@ -208,8 +208,9 @@ export class TrzszClient extends TransferClientBase {
208
208
  this.currentTransfer.path = path
209
209
  // Call directly to ensure 100% is displayed immediately
210
210
  this._doWriteProgress(true)
211
- // Add newline after completion
211
+ // Add newline after completion and reset row tracker for next file
212
212
  this.writeToTerminal('\r\n')
213
+ this._prevProgressRows = 0
213
214
  }
214
215
  this.currentTransfer = null
215
216
  }
@@ -230,20 +231,18 @@ export class TrzszClient extends TransferClientBase {
230
231
  if (!this.currentTransfer || !this.terminal?.term) return
231
232
 
232
233
  const { name, size, transferred, path, serverSpeed } = this.currentTransfer
233
- const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
234
-
235
- // Use server's speed if available, otherwise calculate locally
236
234
  const speed = serverSpeed || 0
237
-
238
- // Use full path if available, otherwise just name
239
235
  const displayName = path || name
240
-
241
- // filesize expects bytes and formats to human readable
242
236
  const formatSize = (bytes) => filesize(bytes)
243
237
 
244
- // Clear line and write progress
245
- const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
246
- this.writeToTerminal(str + '\r')
238
+ this.writeProgressBar({
239
+ name: displayName,
240
+ size,
241
+ transferred,
242
+ speed,
243
+ isComplete,
244
+ formatSize
245
+ })
247
246
  }
248
247
 
249
248
  /**
@@ -180,8 +180,9 @@ export class ZmodemClient extends TransferClientBase {
180
180
  this.currentTransfer.path = path
181
181
  // Call directly to ensure 100% is displayed immediately
182
182
  this._doWriteProgress(true)
183
- // Add newline after completion
183
+ // Add newline after completion and reset row tracker for next file
184
184
  this.writeToTerminal('\r\n')
185
+ this._prevProgressRows = 0
185
186
  }
186
187
  this.currentTransfer = null
187
188
  }
@@ -212,20 +213,18 @@ export class ZmodemClient extends TransferClientBase {
212
213
  if (!this.currentTransfer || !this.terminal?.term) return
213
214
 
214
215
  const { name, size, transferred, path, serverSpeed } = this.currentTransfer
215
- const percent = size > 0 ? Math.floor(transferred * 100 / size) : 100
216
-
217
- // Use server's speed if available, otherwise calculate locally
218
216
  const speed = serverSpeed || 0
219
-
220
- // Use full path if available, otherwise just name
221
217
  const displayName = path || name
222
-
223
- // filesize expects bytes and formats to human readable
224
218
  const formatSize = (bytes) => filesize(bytes)
225
219
 
226
- // Clear line and write progress
227
- const str = `\r\x1b[2K\x1b[32m${displayName}\x1b[0m: ${percent}%, ${formatSize(transferred)}/${formatSize(size)}, ${formatSize(speed)}/s${isComplete ? ' \x1b[32m\x1b[1m[DONE]\x1b[0m' : ''}`
228
- this.writeToTerminal(str + '\r')
220
+ this.writeProgressBar({
221
+ name: displayName,
222
+ size,
223
+ transferred,
224
+ speed,
225
+ isComplete,
226
+ formatSize
227
+ })
229
228
  }
230
229
  }
231
230
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Edit with custom editor - input + button component
3
+ */
4
+
5
+ import { useState } from 'react'
6
+ import { Button, Input, Space } from 'antd'
7
+
8
+ const LS_KEY = 'customEditorCommand'
9
+ const e = window.translate
10
+
11
+ export default function EditWithCustomEditor ({ loading, editWithCustom }) {
12
+ const [editorCommand, setEditorCommand] = useState(
13
+ () => window.localStorage.getItem(LS_KEY) || ''
14
+ )
15
+
16
+ function handleChange (ev) {
17
+ const val = ev.target.value
18
+ setEditorCommand(val)
19
+ window.localStorage.setItem(LS_KEY, val)
20
+ }
21
+
22
+ function handleClick () {
23
+ const cmd = editorCommand.trim()
24
+ if (cmd) {
25
+ editWithCustom(cmd)
26
+ }
27
+ }
28
+
29
+ if (window.et.isWebApp) {
30
+ return null
31
+ }
32
+
33
+ return (
34
+ <Space.Compact className='mg1b'>
35
+ <Button
36
+ type='primary'
37
+ disabled={loading || !editorCommand.trim()}
38
+ onClick={handleClick}
39
+ >
40
+ {e('editWith')}
41
+ </Button>
42
+ <Input
43
+ value={editorCommand}
44
+ onChange={handleChange}
45
+ disabled={loading}
46
+ />
47
+ </Space.Compact>
48
+ )
49
+ }
@@ -18,6 +18,11 @@ export default function SimpleEditor (props) {
18
18
 
19
19
  // When currentMatch changes, highlight the match in textarea
20
20
  useEffect(() => {
21
+ // Only process navigation when explicitly triggered (not when text changes)
22
+ if (!isNavigating) {
23
+ return
24
+ }
25
+
21
26
  if (currentMatch >= 0 && occurrences.length > 0) {
22
27
  const match = occurrences[currentMatch]
23
28
  if (editorRef.current) {
@@ -26,10 +31,8 @@ export default function SimpleEditor (props) {
26
31
  // Set selection range to select the matched text
27
32
  textarea.setSelectionRange(match.start, match.end)
28
33
 
29
- // Only focus the textarea when explicitly navigating between matches
30
- if (isNavigating) {
31
- textarea.focus()
32
- }
34
+ // Focus the textarea when explicitly navigating between matches
35
+ textarea.focus()
33
36
 
34
37
  // Scroll to the selection position
35
38
  // Use setTimeout to ensure the selection is rendered before scrolling
@@ -53,10 +56,17 @@ export default function SimpleEditor (props) {
53
56
  setIsNavigating(false)
54
57
  }, [currentMatch, occurrences])
55
58
 
56
- // Auto-search when keyword changes
59
+ // Auto-search when keyword changes (but not when text is being edited)
57
60
  useEffect(() => {
61
+ // Set navigating to true so first match is highlighted when searching
62
+ setIsNavigating(true)
58
63
  findMatches()
59
- }, [searchKeyword, props.value])
64
+ }, [searchKeyword])
65
+
66
+ // Update matches when text changes, but don't change currentMatch position
67
+ useEffect(() => {
68
+ updateMatchesOnly()
69
+ }, [props.value])
60
70
 
61
71
  // Copy the editor content to clipboard
62
72
  const copyEditorContent = () => {
@@ -87,6 +97,28 @@ export default function SimpleEditor (props) {
87
97
  setCurrentMatch(matches.length ? 0 : -1)
88
98
  }
89
99
 
100
+ // Update matches only (without changing currentMatch position)
101
+ const updateMatchesOnly = () => {
102
+ if (!searchKeyword) {
103
+ setOccurrences([])
104
+ return
105
+ }
106
+
107
+ const matches = []
108
+ const text = props.value || ''
109
+ const escapedKeyword = escapeRegExp(searchKeyword)
110
+ const regex = new RegExp(escapedKeyword, 'gi')
111
+ let match
112
+
113
+ while ((match = regex.exec(text)) !== null) {
114
+ matches.push({
115
+ start: match.index,
116
+ end: match.index + searchKeyword.length
117
+ })
118
+ }
119
+ setOccurrences(matches)
120
+ }
121
+
90
122
  // Handle search action when user presses enter or clicks the search button
91
123
  const handleSearch = (e) => {
92
124
  if (e && e.stopPropagation) {
@@ -5,6 +5,7 @@
5
5
  import { useEffect } from 'react'
6
6
  import { Form, Button } from 'antd'
7
7
  import SimpleEditor from './simple-editor'
8
+ import EditWithCustomEditor from './edit-with-custom-editor'
8
9
 
9
10
  const FormItem = Form.Item
10
11
  const e = window.translate
@@ -38,7 +39,7 @@ export default function TextEditorForm (props) {
38
39
  } = props
39
40
  const popsEdit = {
40
41
  type: 'primary',
41
- className: 'mg3r mg1b',
42
+ className: 'mg1r mg1b',
42
43
  disabled: loading,
43
44
  onClick: props.editWith
44
45
  }
@@ -62,10 +63,6 @@ export default function TextEditorForm (props) {
62
63
  <SimpleEditor />
63
64
  </FormItem>
64
65
  <div className='pd1t pd2b'>
65
- <Button
66
- {...popsEdit}
67
- >{e('editWithSystemEditor')}
68
- </Button>
69
66
  <Button
70
67
  type='primary'
71
68
  className='mg1r mg1b'
@@ -85,6 +82,17 @@ export default function TextEditorForm (props) {
85
82
  >{e('cancel')}
86
83
  </Button>
87
84
  </div>
85
+ <div className='pd1t pd2b'>
86
+ <Button
87
+ {...popsEdit}
88
+ >
89
+ {e('editWithSystemEditor')}
90
+ </Button>
91
+ <EditWithCustomEditor
92
+ loading={loading}
93
+ editWithCustom={props.editWithCustom}
94
+ />
95
+ </div>
88
96
  </Form>
89
97
  )
90
98
  }
@@ -118,6 +118,24 @@ export default class TextEditor extends PureComponent {
118
118
  fileRef.editWithSystemEditor(text)
119
119
  }
120
120
 
121
+ editWithCustom = async (editorCommand) => {
122
+ this.setStateProxy({
123
+ loading: true
124
+ })
125
+ const {
126
+ id, text
127
+ } = this.state
128
+ const fileRef = refs.get(id)
129
+ if (!fileRef) {
130
+ return
131
+ }
132
+ await fileRef.editWithCustomEditor(text, editorCommand)
133
+ .catch(err => {
134
+ this.setStateProxy({ loading: false })
135
+ window.store.onError(err)
136
+ })
137
+ }
138
+
121
139
  cancel = () => {
122
140
  this.setStateProxy({
123
141
  id: '',
@@ -150,7 +168,8 @@ export default class TextEditor extends PureComponent {
150
168
  submit: this.handleSubmit,
151
169
  text,
152
170
  cancel: this.cancel,
153
- editWith: this.editWith
171
+ editWith: this.editWith,
172
+ editWithCustom: this.editWithCustom
154
173
  }
155
174
  return (
156
175
  <Modal
@@ -373,6 +373,9 @@ export default class VncSession extends PureComponent {
373
373
  }
374
374
  rfb.scaleViewport = scaleViewport
375
375
  rfb.clipViewport = clipViewport
376
+ rfb.qualityLevel = qualityLevel
377
+ rfb.compressionLevel = compressionLevel
378
+ rfb.viewOnly = viewOnly
376
379
  this.rfb = rfb
377
380
  }
378
381
 
@@ -14,4 +14,4 @@
14
14
  height: 100% !important
15
15
  object-fit: contain
16
16
  > div
17
- background: transparent !important
17
+ background: transparent !important
@@ -33,18 +33,10 @@ export default Store => {
33
33
  return {
34
34
  term: 'xterm-256color',
35
35
  id: uid(),
36
- type: 'local',
37
- title: 'ssh config: ' + t.title,
36
+ type: 'ssh',
38
37
  color: '#0088cc',
39
- runScripts: [
40
- {
41
- script: `ssh ${t.title}`,
42
- delay: 500
43
- }
44
- ]
38
+ ...t
45
39
  }
46
- }).filter(d => {
47
- return !store.bookmarks.find(t => t.title === d.title)
48
40
  })
49
41
  const ids = bookmarksToAdd.map(d => d.id)
50
42
  let sshConfigGroup = store.bookmarkGroups.find(d => d.id === 'sshConfig')
@@ -61,7 +53,7 @@ export default Store => {
61
53
  ...ids,
62
54
  ...(sshConfigGroup.bookmarkIds || [])
63
55
  ]
64
- })
56
+ }, 'bookmarkGroups')
65
57
  }
66
58
  return store.addItems(bookmarksToAdd, 'bookmarks')
67
59
  }
@@ -345,13 +345,40 @@ export default Store => {
345
345
  return
346
346
  }
347
347
  const { terminalCommandHistory } = window.store
348
- terminalCommandHistory.add(cmd)
348
+ const existing = terminalCommandHistory.get(cmd)
349
+ if (existing) {
350
+ // Use set() to trigger reactivity
351
+ terminalCommandHistory.set(cmd, {
352
+ count: existing.count + 1,
353
+ lastUseTime: new Date().toISOString()
354
+ })
355
+ } else {
356
+ terminalCommandHistory.set(cmd, {
357
+ count: 1,
358
+ lastUseTime: new Date().toISOString()
359
+ })
360
+ }
349
361
  if (terminalCommandHistory.size > 100) {
350
362
  // Delete oldest 20 items when history exceeds 100
351
- const values = Array.from(terminalCommandHistory.values())
352
- for (let i = 0; i < 20 && i < values.length; i++) {
353
- terminalCommandHistory.delete(values[i])
363
+ const entries = Array.from(terminalCommandHistory.entries())
364
+ entries.sort((a, b) => new Date(a[1].lastUseTime).getTime() - new Date(b[1].lastUseTime).getTime())
365
+ for (let i = 0; i < 20 && i < entries.length; i++) {
366
+ terminalCommandHistory.delete(entries[i][0])
354
367
  }
355
368
  }
356
369
  })
370
+
371
+ Store.prototype.deleteCmdHistory = function (cmd) {
372
+ const { terminalCommandHistory } = window.store
373
+ terminalCommandHistory.delete(cmd)
374
+ }
375
+
376
+ Store.prototype.clearAllCmdHistory = function () {
377
+ window.store.terminalCommandHistory = new Map()
378
+ }
379
+
380
+ Store.prototype.runCmdFromHistory = function (cmd) {
381
+ window.store.runQuickCommand(cmd)
382
+ window.store.addCmdHistory(cmd)
383
+ }
357
384
  }
@@ -74,7 +74,32 @@ export default () => {
74
74
  addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
75
75
  openResolutionEdit: false,
76
76
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
77
- terminalCommandHistory: new Set(ls.getItemJSON(cmdHistoryKey, [])),
77
+ // terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
78
+ // Load from localStorage and migrate from old format (Set of strings) if needed
79
+ terminalCommandHistory: (() => {
80
+ const savedData = ls.getItemJSON(cmdHistoryKey, [])
81
+ const map = new Map()
82
+ if (Array.isArray(savedData)) {
83
+ // Check if old format (array of strings) or new format (array of objects)
84
+ if (savedData.length > 0 && typeof savedData[0] === 'string') {
85
+ // Old format: migrate to new format
86
+ savedData.forEach(cmd => {
87
+ map.set(cmd, { count: 1, lastUseTime: new Date().toISOString() })
88
+ })
89
+ } else {
90
+ // New format: array of {cmd, count, lastUseTime}
91
+ savedData.forEach(item => {
92
+ if (item.cmd) {
93
+ map.set(item.cmd, {
94
+ count: item.count || 1,
95
+ lastUseTime: item.lastUseTime || new Date().toISOString()
96
+ })
97
+ }
98
+ })
99
+ }
100
+ }
101
+ return map
102
+ })(),
78
103
 
79
104
  // workspaces
80
105
  workspaces: [],
@@ -176,7 +176,7 @@ class Store {
176
176
 
177
177
  get terminalCommandSuggestions () {
178
178
  const { store } = window
179
- const historyCommands = Array.from(store.terminalCommandHistory)
179
+ const historyCommands = Array.from(store.terminalCommandHistory.keys())
180
180
  const batchInputCommands = store.batchInputs
181
181
  const quickCommands = store.quickCommands.reduce(
182
182
  (p, q) => {
@@ -35,9 +35,8 @@ async function fetchData (type, func, args, token, proxy) {
35
35
  }
36
36
 
37
37
  function updateSyncServerStatusFromGist (store, gist, type) {
38
- const status = parseJsonSafe(
39
- get(gist, 'files["electerm-status.json"].content')
40
- )
38
+ const statusContent = get(gist, 'files["electerm-status.json"].content')
39
+ const status = statusContent ? parseJsonSafe(statusContent) : undefined
41
40
  store.syncServerStatus[type] = status
42
41
  }
43
42
 
@@ -145,7 +145,14 @@ export default store => {
145
145
  }).start()
146
146
 
147
147
  autoRun(() => {
148
- ls.setItemJSON(cmdHistoryKey, Array.from(store.terminalCommandHistory))
148
+ const history = store.terminalCommandHistory
149
+ // Save in new format: array of {cmd, count, lastUseTime}
150
+ const data = Array.from(history.entries()).map(([cmd, info]) => ({
151
+ cmd,
152
+ count: info.count,
153
+ lastUseTime: info.lastUseTime
154
+ }))
155
+ ls.setItemJSON(cmdHistoryKey, data)
149
156
  return store.terminalCommandHistory
150
157
  }).start()
151
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.13.6",
3
+ "version": "2.16.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,24 +0,0 @@
1
- import { Tooltip } from 'antd'
2
-
3
- export default function SshConfigItem (props) {
4
- const { item } = props
5
-
6
- const generateTooltipContent = (item) => {
7
- return Object.entries(item)
8
- .filter(([key]) => key !== 'id')
9
- .map(([key, value]) => (
10
- <div key={key}>
11
- <b className='mg1r'>{key}:</b>
12
- <span>{value}</span>
13
- </div>
14
- ))
15
- }
16
-
17
- return (
18
- <Tooltip title={generateTooltipContent(item)}>
19
- <div className='elli pd1y pd2x'>
20
- ssh {item.title}
21
- </div>
22
- </Tooltip>
23
- )
24
- }