@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.
- package/README.md +5 -5
- package/client/common/bookmark-schemas.js +4 -0
- package/client/common/constants.js +3 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +2 -0
- package/client/common/parse-quick-connect.js +1 -0
- package/client/common/parse-vv.js +119 -25
- package/client/common/parse-widget-log.js +57 -0
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +26 -7
- package/client/components/ai/agent.js +205 -45
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +133 -84
- package/client/components/ai/ai-chat.jsx +135 -30
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +43 -16
- package/client/components/ai/ai-config.styl +20 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +5 -6
- package/client/components/ai/ai-presets.js +1 -16
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/batch-op/batch-op-runner.jsx +1 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
- package/client/components/bookmark-form/common/vv-file-field.jsx +3 -2
- package/client/components/bookmark-form/config/spice.js +21 -0
- package/client/components/bookmark-form/config/ssh.js +1 -0
- package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
- package/client/components/file-transfer/transfer.jsx +3 -119
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +7 -1
- package/client/components/layout/layout.jsx +12 -19
- package/client/components/main/main.jsx +15 -1
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-commands-box.jsx +180 -83
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/session/session-control.jsx +35 -2
- package/client/components/session/session-control.styl +5 -0
- package/client/components/session/session.jsx +13 -0
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/setting-panel/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- package/client/components/side-panel-r/side-panel-r.jsx +11 -4
- package/client/components/sidebar/bookmark-select.jsx +14 -2
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/terminal/mixins/term-touch.js +18 -0
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/components/widgets/widget-control.jsx +4 -2
- package/client/components/widgets/widget-instance-detail.jsx +370 -0
- package/client/components/widgets/widget-instance.jsx +30 -13
- package/client/components/widgets/widget-instances.jsx +3 -1
- package/client/components/widgets/widgets-list.jsx +8 -0
- package/client/components/widgets/widgets.styl +100 -0
- package/client/store/common.js +200 -54
- package/client/store/init-state.js +15 -0
- package/client/store/load-data.js +73 -0
- package/client/store/store.js +6 -0
- package/client/store/watch.js +3 -1
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
onClick={handleToggleAutoRun}
|
|
91
|
-
/>
|
|
92
|
-
</Tooltip>
|
|
93
|
-
<Popconfirm
|
|
94
|
-
{...popProps}
|
|
99
|
+
<div
|
|
100
|
+
className='wil-item-actions'
|
|
101
|
+
onClick={stopPropagation}
|
|
95
102
|
>
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|