@electerm/electerm-react 5.5.25 → 5.5.56

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 (66) hide show
  1. package/README.md +5 -5
  2. package/client/common/bookmark-schemas.js +4 -0
  3. package/client/common/constants.js +3 -0
  4. package/client/common/create-title.jsx +5 -1
  5. package/client/common/default-setting.js +2 -0
  6. package/client/common/parse-quick-connect.js +1 -0
  7. package/client/common/parse-vv.js +119 -25
  8. package/client/common/parse-widget-log.js +57 -0
  9. package/client/common/vv-to-tab.js +120 -0
  10. package/client/components/ai/agent-tool-call-card.jsx +26 -7
  11. package/client/components/ai/agent.js +205 -45
  12. package/client/components/ai/ai-auto-compress.js +119 -0
  13. package/client/components/ai/ai-chat-history-item.jsx +133 -84
  14. package/client/components/ai/ai-chat.jsx +135 -30
  15. package/client/components/ai/ai-compress.js +95 -0
  16. package/client/components/ai/ai-config-props.js +12 -1
  17. package/client/components/ai/ai-config.jsx +43 -16
  18. package/client/components/ai/ai-config.styl +20 -0
  19. package/client/components/ai/ai-context-indicator.jsx +83 -0
  20. package/client/components/ai/ai-context.js +272 -0
  21. package/client/components/ai/ai-output.jsx +5 -6
  22. package/client/components/ai/ai-presets.js +1 -16
  23. package/client/components/ai/ai.styl +105 -6
  24. package/client/components/ai/copy-icon.jsx +31 -0
  25. package/client/components/batch-op/batch-op-runner.jsx +1 -0
  26. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  27. package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
  28. package/client/components/bookmark-form/common/vv-file-field.jsx +3 -2
  29. package/client/components/bookmark-form/config/spice.js +21 -0
  30. package/client/components/bookmark-form/config/ssh.js +1 -0
  31. package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
  32. package/client/components/file-transfer/transfer.jsx +3 -119
  33. package/client/components/footer/cmd-history.jsx +279 -65
  34. package/client/components/footer/cmd-history.styl +68 -1
  35. package/client/components/footer/footer-entry.jsx +7 -1
  36. package/client/components/layout/layout.jsx +12 -19
  37. package/client/components/main/main.jsx +15 -1
  38. package/client/components/quick-commands/qm.styl +58 -0
  39. package/client/components/quick-commands/quick-commands-box.jsx +180 -83
  40. package/client/components/quick-commands/quick-commands-select.jsx +11 -0
  41. package/client/components/session/session-control.jsx +35 -2
  42. package/client/components/session/session-control.styl +5 -0
  43. package/client/components/session/session.jsx +13 -0
  44. package/client/components/setting-panel/setting-common.jsx +2 -0
  45. package/client/components/setting-panel/tab-widgets.jsx +27 -7
  46. package/client/components/sftp/file-item.jsx +0 -13
  47. package/client/components/side-panel-r/side-panel-r.jsx +11 -4
  48. package/client/components/sidebar/bookmark-select.jsx +14 -2
  49. package/client/components/sidebar/history-item.jsx +20 -4
  50. package/client/components/sidebar/history.jsx +7 -0
  51. package/client/components/terminal/mixins/term-touch.js +18 -0
  52. package/client/components/tree-list/tree-list.jsx +31 -3
  53. package/client/components/widgets/widget-control.jsx +4 -2
  54. package/client/components/widgets/widget-instance-detail.jsx +370 -0
  55. package/client/components/widgets/widget-instance.jsx +30 -13
  56. package/client/components/widgets/widget-instances.jsx +3 -1
  57. package/client/components/widgets/widgets-list.jsx +8 -0
  58. package/client/components/widgets/widgets.styl +100 -0
  59. package/client/store/common.js +200 -54
  60. package/client/store/init-state.js +15 -0
  61. package/client/store/load-data.js +73 -0
  62. package/client/store/store.js +6 -0
  63. package/client/store/watch.js +3 -1
  64. package/client/store/widgets.js +23 -5
  65. package/package.json +1 -1
  66. package/client/components/file-transfer/zip.js +0 -42
@@ -2,11 +2,13 @@
2
2
  * bookmark select
3
3
  */
4
4
 
5
+ import { useState } from 'react'
5
6
  import { auto } from 'manate/react'
6
7
  import TreeList from '../tree-list/tree-list'
7
8
 
8
9
  export default auto(function BookmarkSelect (props) {
9
10
  const { store, from, autoFocus } = props
11
+ const [activeItemId, setActiveItemId] = useState('')
10
12
  const {
11
13
  listStyle,
12
14
  openedSideBar,
@@ -19,7 +21,7 @@ export default auto(function BookmarkSelect (props) {
19
21
  if (from === 'sidebar' && openedSideBar !== 'bookmarks') {
20
22
  return null
21
23
  }
22
- const onClickItem = (item) => {
24
+ const openBookmark = (item) => {
23
25
  // A pinned (docked) panel stays open; anything else — including mobile,
24
26
  // where pinning is not a reachable mode — dismisses on selection.
25
27
  if (!(store.pinned && !store.isMobile)) {
@@ -27,10 +29,20 @@ export default auto(function BookmarkSelect (props) {
27
29
  }
28
30
  store.onSelectBookmark(item.id)
29
31
  }
32
+ // doubleClickToOpenBookmark: a single click only marks the row, the session
33
+ // opens on a double click. Touch devices have no double click, so the setting
34
+ // is ignored there. This is the sidebar list only — the settings bookmarks tab
35
+ // keeps selecting for editing on a single click.
36
+ const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
37
+ !store.isTouchDevice
30
38
  const base = {
31
39
  bookmarks: bookmarks || [],
32
40
  type: 'bookmarks',
33
- onClickItem,
41
+ activeItemId: openOnDoubleClick ? activeItemId : undefined,
42
+ onClickItem: openOnDoubleClick
43
+ ? item => setActiveItemId(item.id)
44
+ : openBookmark,
45
+ onDoubleClickItem: openOnDoubleClick ? openBookmark : undefined,
34
46
  listStyle,
35
47
  staticList: true
36
48
  }
@@ -1,4 +1,5 @@
1
- import React, { useCallback, useEffect, useRef } from 'react'
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import classnames from 'classnames'
2
3
  import createTitle, { createTitleWithTag } from '../../common/create-title'
3
4
  import { DeleteOutlined, BookFilled } from '@ant-design/icons'
4
5
  import { refsStatic } from '../common/ref'
@@ -6,18 +7,32 @@ import { refsStatic } from '../common/ref'
6
7
  export default function HistoryItem (props) {
7
8
  const { store } = window
8
9
  const {
9
- item
10
+ item,
11
+ openOnDoubleClick
10
12
  } = props
13
+ const [active, setActive] = useState(false)
11
14
  const timeoutRef = useRef(null)
12
15
 
13
16
  const handleClick = useCallback(() => {
17
+ // with doubleClickToOpenBookmark on, a click only marks the row — the
18
+ // session opens on the double click below
19
+ if (openOnDoubleClick) {
20
+ setActive(true)
21
+ return
22
+ }
14
23
  if (timeoutRef.current) {
15
24
  clearTimeout(timeoutRef.current)
16
25
  }
17
26
  timeoutRef.current = setTimeout(() => {
18
27
  store.onSelectHistory(item.tab)
19
28
  }, 10)
20
- }, [item.tab])
29
+ }, [item.tab, openOnDoubleClick])
30
+
31
+ const handleDoubleClick = useCallback(() => {
32
+ if (openOnDoubleClick) {
33
+ store.onSelectHistory(item.tab)
34
+ }
35
+ }, [item.tab, openOnDoubleClick])
21
36
 
22
37
  useEffect(() => {
23
38
  return () => {
@@ -47,9 +62,10 @@ export default function HistoryItem (props) {
47
62
  const tt = createTitle(item.tab)
48
63
  return (
49
64
  <div
50
- className='item-list-unit'
65
+ className={classnames('item-list-unit', { active })}
51
66
  title={tt}
52
67
  onClick={handleClick}
68
+ onDoubleClick={handleDoubleClick}
53
69
  >
54
70
  <div className='elli pd1y pd2x'>
55
71
  {title}
@@ -25,6 +25,12 @@ export default auto(function HistoryPanel (props) {
25
25
  const {
26
26
  history
27
27
  } = store
28
+ // doubleClickToOpenBookmark: a single click only marks the row, the session
29
+ // opens on a double click. Touch devices have no double click, so the setting
30
+ // is ignored there. Read here rather than in HistoryItem because this
31
+ // component is `auto` — it re-renders when the input modality flips.
32
+ const openOnDoubleClick = !!store.config.doubleClickToOpenBookmark &&
33
+ !store.isTouchDevice
28
34
  let arr = store.config.disableConnectionHistory ? [] : history
29
35
  if (sortByFrequency) {
30
36
  arr = [...arr].sort((a, b) => { return b.count - a.count })
@@ -75,6 +81,7 @@ export default auto(function HistoryPanel (props) {
75
81
  <HistoryItem
76
82
  key={item.id}
77
83
  item={item}
84
+ openOnDoubleClick={openOnDoubleClick}
78
85
  />
79
86
  )
80
87
  })
@@ -41,6 +41,10 @@
41
41
  * `stopPropagation()` is enough to veto xterm's scroll for the duration of
42
42
  * the drag. `preventDefault()` is not an option: React listens to
43
43
  * `touchmove` passively, so it is ignored (with a console warning).
44
+ *
45
+ * A long press is not the only way in: the toolbar shows a touch-only menu
46
+ * icon next to the three-dot toggle (see session-control.jsx), which opens the
47
+ * same menu through openContextMenuAtPoint() below.
44
48
  */
45
49
 
46
50
  const DEFAULT_WORD_SEPARATOR = './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
@@ -505,6 +509,20 @@ export const touchMixin = {
505
509
  target.dispatchEvent(event)
506
510
  },
507
511
 
512
+ /**
513
+ * Opens the menu from the toolbar icon, which lives outside the antd
514
+ * dropdown's trigger element — so the event is dispatched on the terminal's
515
+ * own node and bubbles up to it. The icon's corner is passed in as the point
516
+ * to drop the menu from, rather than the finger that tapped it.
517
+ */
518
+ openContextMenuAtPoint (clientX, clientY) {
519
+ this.openContextMenuAt(
520
+ clientX,
521
+ clientY,
522
+ this.domRef?.current || this.term?.element
523
+ )
524
+ },
525
+
508
526
  cancelLongPress () {
509
527
  clearTimeout(this.longPressTimer)
510
528
  this.longPressTimer = null
@@ -158,7 +158,11 @@ export default class ItemListTree extends Component {
158
158
  : matchedRowKeys[0]
159
159
  const row = rows.find(item => item.key === rowKey)
160
160
  if (row?.item?.id) {
161
- this.selectBookmarkById(row.item.id)
161
+ // Enter is a deliberate activation, so it opens the bookmark even when
162
+ // doubleClickToOpenBookmark makes a single click only mark the row. The
163
+ // settings bookmarks tab passes no onDoubleClickItem, so there Enter
164
+ // still selects for editing, exactly like a click.
165
+ this.selectBookmarkById(row.item.id, this.props.onDoubleClickItem)
162
166
  }
163
167
  }
164
168
  }
@@ -404,7 +408,9 @@ export default class ItemListTree extends Component {
404
408
  }
405
409
  }
406
410
 
407
- selectBookmarkById = (id) => {
411
+ // `act` overrides what a selection does; only the Enter path passes it, so a
412
+ // click keeps whatever meaning the consumer gave onClickItem.
413
+ selectBookmarkById = (id, act) => {
408
414
  const { store } = window
409
415
  store.storeAssign({
410
416
  currentBookmarkGroupId: findBookmarkGroupId(store.bookmarkGroups, id)
@@ -414,7 +420,28 @@ export default class ItemListTree extends Component {
414
420
  d => d.id === id
415
421
  )
416
422
  if (bookmark) {
417
- this.props.onClickItem(bookmark)
423
+ (act || this.props.onClickItem)(bookmark)
424
+ }
425
+ }
426
+
427
+ handleDoubleClick = (e) => {
428
+ // only the static list (the sidebar) passes this, so the bookmark edit page
429
+ // keeps its single click to select behaviour
430
+ const { onDoubleClickItem, bookmarks } = this.props
431
+ if (!onDoubleClickItem) {
432
+ return
433
+ }
434
+ const item = findParentBySel(e.target, '.tree-item')
435
+ // a group toggles on a single click, so a double click would just toggle it
436
+ // twice and land back where it started
437
+ if (!item || item.getAttribute('data-is-group') === 'true') {
438
+ return
439
+ }
440
+ const bookmark = bookmarks.find(
441
+ d => d.id === item.getAttribute('data-item-id')
442
+ )
443
+ if (bookmark) {
444
+ onDoubleClickItem(bookmark)
418
445
  }
419
446
  }
420
447
 
@@ -917,6 +944,7 @@ export default class ItemListTree extends Component {
917
944
  className='item-list-wrap'
918
945
  style={listStyle}
919
946
  ref={this.listRef}
947
+ onDoubleClick={this.handleDoubleClick}
920
948
  >
921
949
  {this.renderVirtualTreeContent(rows, editor)}
922
950
  {this.renderEditorOverlay(editor)}
@@ -46,9 +46,11 @@ export default function WidgetControl ({ formData, widgetInstancesLength }) {
46
46
  title: `${widget.info.name} (${result.instanceId})`,
47
47
  widgetId: result.widgetId,
48
48
  serverInfo: result.serverInfo,
49
- config
49
+ config,
50
+ startedAt: Date.now()
50
51
  }
51
- window.store.widgetInstances.push(instance)
52
+ // assigned, not pushed: see deleteWidgetInstance in store/widgets.js
53
+ window.store.widgetInstances = [...window.store.widgetInstances, instance]
52
54
  if (config.autoRun) {
53
55
  window.store.toggleAutoRunWidget(instance)
54
56
  }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Detail view for a widget instance: what it is, and what it has been doing.
3
+ *
4
+ * Shown in the right column of the widgets panel (Settings -> Widgets) when an
5
+ * instance is picked from the "Running instances" list, in place of the run
6
+ * form. The instance keeps this panel after it stops — being able to read the
7
+ * log of an instance that died is the whole point of it.
8
+ *
9
+ * Both panes come out of the instance's log file (see
10
+ * app/widgets/instance-log.js): on open, and again whenever Reload is pressed.
11
+ * Deliberately not on a timer — a log that scrolls under you is harder to read
12
+ * than one you refresh when you are ready.
13
+ *
14
+ * The strings here are English: the keys this panel needs do not exist yet, and
15
+ * a new key only reaches the app through an electerm-locales release plus a dep
16
+ * bump (see the locales notes). `e()` is used only for keys that do exist.
17
+ */
18
+ import { useCallback, useEffect, useRef, useState } from 'react'
19
+ import {
20
+ Alert,
21
+ Button,
22
+ Descriptions,
23
+ Popconfirm,
24
+ Segmented,
25
+ Space,
26
+ Tag,
27
+ Tooltip,
28
+ Typography
29
+ } from 'antd'
30
+ import {
31
+ CopyOutlined,
32
+ DeleteOutlined,
33
+ ReloadOutlined,
34
+ StopOutlined
35
+ } from '@ant-design/icons'
36
+ import { auto } from 'manate/react'
37
+ import { copy } from '../../common/clipboard'
38
+ import parseWidgetLog from '../../common/parse-widget-log'
39
+ import './widgets.styl'
40
+
41
+ const e = window.translate
42
+
43
+ const SECRET_KEY = /password|passwd|secret|token|apikey|api_key/i
44
+ const CONN_MARK = { ok: '✓', fail: '✗', info: '•' }
45
+
46
+ function pad (n, len = 2) {
47
+ return String(n).padStart(len, '0')
48
+ }
49
+
50
+ function formatTime (ts) {
51
+ const d = new Date(ts)
52
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
53
+ }
54
+
55
+ // the URL of a widget that needs credentials carries them; the panel gets
56
+ // screenshotted, so hide the password here (copy still copies the real one)
57
+ function maskUrl (url) {
58
+ return String(url || '').replace(/\/\/([^:/@]+):([^@/]+)@/, '//$1:••••••@')
59
+ }
60
+
61
+ function displayValue (key, value) {
62
+ if (SECRET_KEY.test(key)) {
63
+ return value ? '••••••' : ''
64
+ }
65
+ if (typeof value === 'boolean') {
66
+ return value ? 'true' : 'false'
67
+ }
68
+ return value === undefined || value === null ? '' : String(value)
69
+ }
70
+
71
+ export default auto(function WidgetInstanceDetail ({ instance, store }) {
72
+ const [pane, setPane] = useState('log')
73
+ const [data, setData] = useState({ logs: [], events: [] })
74
+ // which confirm popup is open ('' | 'clear' | 'stop'), so its tooltip can
75
+ // step aside — both are anchored to the same button, and the tooltip would
76
+ // otherwise sit on top of the confirm button and swallow the click
77
+ const [confirming, setConfirming] = useState('')
78
+ const [status, setStatus] = useState({
79
+ loaded: false,
80
+ loading: false,
81
+ exists: true,
82
+ error: ''
83
+ })
84
+ const paneRef = useRef(null)
85
+
86
+ const running = store.widgetInstances.some(w => w.id === instance.id)
87
+ const logPath = store.widgetLogPath && window.reqs?.path?.join
88
+ ? window.reqs.path.join(store.widgetLogPath, `${instance.id}.log`)
89
+ : ''
90
+
91
+ const readLog = useCallback(async () => {
92
+ if (!logPath) {
93
+ setStatus({ loaded: true, loading: false, exists: false, error: '' })
94
+ return
95
+ }
96
+ setStatus(s => ({ ...s, loading: true }))
97
+ try {
98
+ // read straight through: fs/promises has no `exists`, and the bridge
99
+ // rejects unknown helpers — so a missing file is detected from ENOENT
100
+ // rather than probed for, which also saves a round trip
101
+ const text = await window.fs.readFile(logPath)
102
+ setData(parseWidgetLog(text))
103
+ setStatus({ loaded: true, loading: false, exists: true, error: '' })
104
+ } catch (err) {
105
+ const msg = (err && err.message) || String(err)
106
+ if (/ENOENT|no such file/i.test(msg)) {
107
+ // the normal first state of a fresh instance, not a failure
108
+ setData({ logs: [], events: [] })
109
+ setStatus({ loaded: true, loading: false, exists: false, error: '' })
110
+ return
111
+ }
112
+ setStatus({ loaded: true, loading: false, exists: true, error: msg })
113
+ }
114
+ }, [logPath])
115
+
116
+ useEffect(() => {
117
+ readLog()
118
+ }, [readLog])
119
+
120
+ // newest entry is at the bottom, so land there after every load
121
+ useEffect(() => {
122
+ const el = paneRef.current
123
+ if (el) {
124
+ el.scrollTop = el.scrollHeight
125
+ }
126
+ }, [data, pane])
127
+
128
+ const clearLog = async () => {
129
+ if (!logPath) {
130
+ return
131
+ }
132
+ try {
133
+ // the bridge resolves false instead of rejecting when the write fails
134
+ const ok = await window.fs.writeFile(logPath, '')
135
+ if (ok === false) {
136
+ throw new Error('could not write ' + logPath)
137
+ }
138
+ setData({ logs: [], events: [] })
139
+ setStatus(s => ({ ...s, exists: true, error: '' }))
140
+ } catch (err) {
141
+ setStatus(s => ({ ...s, error: err.message || String(err) }))
142
+ }
143
+ }
144
+
145
+ // Stopping is the one log change the user causes directly, so refresh after
146
+ // it — that is not the polling the panel is avoiding, it is the panel
147
+ // agreeing with the button that was just pressed.
148
+ const stopInstance = async () => {
149
+ await store.stopWidget(instance.id)
150
+ readLog()
151
+ }
152
+
153
+ const infoItems = [
154
+ {
155
+ key: 'widget',
156
+ label: 'Widget',
157
+ children: instance.widgetId
158
+ },
159
+ {
160
+ key: 'instance',
161
+ label: 'Instance',
162
+ children: <span className='wil-mono'>{instance.id}</span>
163
+ },
164
+ {
165
+ key: 'status',
166
+ label: 'Status',
167
+ children: running ? <Tag color='green'>running</Tag> : <Tag>stopped</Tag>
168
+ }
169
+ ]
170
+ if (instance.serverInfo && instance.serverInfo.url) {
171
+ infoItems.push({
172
+ key: 'url',
173
+ label: 'URL',
174
+ children: (
175
+ <span className='wil-mono' title={maskUrl(instance.serverInfo.url)}>
176
+ {maskUrl(instance.serverInfo.url)}
177
+ </span>
178
+ )
179
+ })
180
+ }
181
+ if (instance.serverInfo && instance.serverInfo.path) {
182
+ infoItems.push({
183
+ key: 'path',
184
+ label: 'Path',
185
+ children: <span className='wil-mono'>{instance.serverInfo.path}</span>
186
+ })
187
+ }
188
+ if (instance.startedAt) {
189
+ infoItems.push({
190
+ key: 'startedAt',
191
+ label: 'Started',
192
+ children: new Date(instance.startedAt).toLocaleString()
193
+ })
194
+ }
195
+ for (const key of Object.keys(instance.config || {})) {
196
+ if (key === 'autoRun') {
197
+ continue
198
+ }
199
+ infoItems.push({
200
+ key: 'config-' + key,
201
+ label: key,
202
+ children: <span className='wil-mono'>{displayValue(key, instance.config[key])}</span>
203
+ })
204
+ }
205
+
206
+ let emptyText = 'Nothing logged yet. Press reload after the widget has been used.'
207
+ if (!logPath) {
208
+ emptyText = 'The widget log directory is not available in this environment.'
209
+ } else if (!status.exists) {
210
+ emptyText = 'No log file yet — this instance has not written anything.'
211
+ }
212
+
213
+ const renderPane = () => {
214
+ const isLog = pane === 'log'
215
+ const entries = isLog ? data.logs : data.events
216
+ return (
217
+ <>
218
+ {
219
+ status.error
220
+ ? (
221
+ <Alert
222
+ className='mg1y'
223
+ type='error'
224
+ showIcon
225
+ message={status.error}
226
+ />
227
+ )
228
+ : null
229
+ }
230
+ <div className='wil-pane' ref={paneRef}>
231
+ {entries.map((entry, i) => {
232
+ const state = isLog
233
+ ? entry.level
234
+ : (entry.ok === null ? 'info' : (entry.ok ? 'ok' : 'fail'))
235
+ return (
236
+ <div
237
+ key={i}
238
+ className={'wil-line wil-' + state}
239
+ >
240
+ <span
241
+ className='wil-time'
242
+ title={new Date(entry.ts).toISOString()}
243
+ >
244
+ {formatTime(entry.ts)}
245
+ </span>
246
+ {isLog
247
+ ? null
248
+ : <span className='wil-mark'>{CONN_MARK[state]}</span>}
249
+ {isLog ? null : <span className='wil-type'>{entry.type}</span>}
250
+ {isLog || !entry.from
251
+ ? null
252
+ : <span className='wil-from'>{entry.from}</span>}
253
+ <span className='wil-msg'>{entry.msg}</span>
254
+ </div>
255
+ )
256
+ })}
257
+ {entries.length ? null : <div className='wil-empty'>{emptyText}</div>}
258
+ </div>
259
+ {logPath
260
+ ? (
261
+ <div className='wil-path elli' title={logPath}>
262
+ Log file: {logPath}
263
+ </div>
264
+ )
265
+ : null}
266
+ </>
267
+ )
268
+ }
269
+
270
+ return (
271
+ <div className='widget-instance-detail'>
272
+ <div className='wil-head'>
273
+ <Typography.Text
274
+ strong
275
+ ellipsis
276
+ className='wil-head-title'
277
+ title={instance.title}
278
+ >
279
+ {instance.title}
280
+ </Typography.Text>
281
+ <Space size={4}>
282
+ <Tooltip title='Reload log'>
283
+ <Button
284
+ size='small'
285
+ icon={<ReloadOutlined />}
286
+ loading={status.loading}
287
+ onClick={readLog}
288
+ />
289
+ </Tooltip>
290
+ {instance.serverInfo && instance.serverInfo.url
291
+ ? (
292
+ <Tooltip title='Copy URL'>
293
+ <Button
294
+ size='small'
295
+ icon={<CopyOutlined />}
296
+ onClick={() => copy(instance.serverInfo.url)}
297
+ />
298
+ </Tooltip>
299
+ )
300
+ : null}
301
+ {logPath
302
+ ? (
303
+ <Popconfirm
304
+ title='Clear this instance log?'
305
+ okText='Clear'
306
+ cancelText={e('cancel')}
307
+ onOpenChange={open => setConfirming(open ? 'clear' : '')}
308
+ onConfirm={clearLog}
309
+ >
310
+ <Tooltip title='Clear log' open={confirming === 'clear' ? false : undefined}>
311
+ <Button size='small' icon={<DeleteOutlined />} />
312
+ </Tooltip>
313
+ </Popconfirm>
314
+ )
315
+ : null}
316
+ {running
317
+ ? (
318
+ <Popconfirm
319
+ title='Stop this widget?'
320
+ okText='Stop'
321
+ cancelText={e('cancel')}
322
+ onOpenChange={open => setConfirming(open ? 'stop' : '')}
323
+ onConfirm={stopInstance}
324
+ >
325
+ <Tooltip title='Stop' open={confirming === 'stop' ? false : undefined}>
326
+ <Button
327
+ size='small'
328
+ danger
329
+ icon={<StopOutlined />}
330
+ />
331
+ </Tooltip>
332
+ </Popconfirm>
333
+ )
334
+ : (
335
+ <Button
336
+ size='small'
337
+ onClick={() => store.closeWidgetInstance()}
338
+ >
339
+ Back
340
+ </Button>
341
+ )}
342
+ </Space>
343
+ </div>
344
+ <Descriptions
345
+ className='wil-descriptions'
346
+ size='small'
347
+ bordered
348
+ column={1}
349
+ items={infoItems}
350
+ />
351
+ <Segmented
352
+ className='wil-switch'
353
+ size='small'
354
+ value={pane}
355
+ onChange={setPane}
356
+ options={[
357
+ {
358
+ label: `Running log (${data.logs.length})`,
359
+ value: 'log'
360
+ },
361
+ {
362
+ label: `Connections (${data.events.length})`,
363
+ value: 'conn'
364
+ }
365
+ ]}
366
+ />
367
+ {renderPane()}
368
+ </div>
369
+ )
370
+ })
@@ -11,10 +11,11 @@ import { auto } from 'manate/react'
11
11
 
12
12
  const e = window.translate
13
13
 
14
- export default auto(function WidgetInstance ({ item }) {
14
+ export default auto(function WidgetInstance ({ item, active, onClick }) {
15
15
  const { id, title, serverInfo, autoRun } = item
16
16
  const cls = classnames('item-list-unit', {
17
- 'autorun-active': autoRun
17
+ 'autorun-active': autoRun,
18
+ active
18
19
  })
19
20
  const delProps = {
20
21
  title: e('del'),
@@ -28,6 +29,16 @@ export default auto(function WidgetInstance ({ item }) {
28
29
  function onConfirm () {
29
30
  window.store.stopWidget(id)
30
31
  }
32
+ // the row itself opens the instance detail, so the action icons must not
33
+ // also trigger it
34
+ function stopPropagation (evt) {
35
+ evt.stopPropagation()
36
+ }
37
+ function handleClick () {
38
+ if (onClick) {
39
+ onClick(item)
40
+ }
41
+ }
31
42
  const popProps = {
32
43
  title: e('del') + '?',
33
44
  onConfirm,
@@ -45,7 +56,7 @@ export default auto(function WidgetInstance ({ item }) {
45
56
  }
46
57
  const popoverContent = serverInfo
47
58
  ? (
48
- <div>
59
+ <div className='wil-pop-info'>
49
60
  <div style={{ display: 'flex', alignItems: 'center' }}>
50
61
  <span>URL: {serverInfo.url}</span>
51
62
  <CopyOutlined
@@ -70,6 +81,7 @@ export default auto(function WidgetInstance ({ item }) {
70
81
  <div
71
82
  key={id}
72
83
  className={cls}
84
+ onClick={handleClick}
73
85
  >
74
86
  {
75
87
  serverInfo
@@ -84,17 +96,22 @@ export default auto(function WidgetInstance ({ item }) {
84
96
  )
85
97
  : titleDiv
86
98
  }
87
- <Tooltip title='Toggle auto-run'>
88
- <ThunderboltOutlined
89
- className='pointer list-item-autorun'
90
- onClick={handleToggleAutoRun}
91
- />
92
- </Tooltip>
93
- <Popconfirm
94
- {...popProps}
99
+ <div
100
+ className='wil-item-actions'
101
+ onClick={stopPropagation}
95
102
  >
96
- {icon}
97
- </Popconfirm>
103
+ <Tooltip title='Toggle auto-run'>
104
+ <ThunderboltOutlined
105
+ className='pointer list-item-autorun'
106
+ onClick={handleToggleAutoRun}
107
+ />
108
+ </Tooltip>
109
+ <Popconfirm
110
+ {...popProps}
111
+ >
112
+ {icon}
113
+ </Popconfirm>
114
+ </div>
98
115
  </div>
99
116
  )
100
117
  })
@@ -1,10 +1,12 @@
1
1
  import WidgetInstance from './widget-instance'
2
2
 
3
- export default function WidgetInstances ({ widgetInstances }) {
3
+ export default function WidgetInstances ({ widgetInstances, activeItemId, onClickItem }) {
4
4
  return widgetInstances.map(item => (
5
5
  <WidgetInstance
6
6
  key={item.id}
7
7
  item={item}
8
+ active={activeItemId === item.id}
9
+ onClick={onClickItem}
8
10
  />
9
11
  ))
10
12
  }