@electerm/electerm-react 3.9.15 → 3.11.0
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/client/common/bookmark-schemas.js +164 -0
- package/client/common/default-setting.js +3 -2
- package/client/common/ws.js +25 -6
- package/client/common/zod.js +180 -0
- package/client/components/ai/agent-tool-call-card.jsx +90 -0
- package/client/components/ai/agent-tools.js +193 -0
- package/client/components/ai/agent.js +159 -0
- package/client/components/ai/ai-chat-entry.jsx +11 -0
- package/client/components/ai/ai-chat-history-item.jsx +48 -2
- package/client/components/ai/ai-chat.jsx +25 -6
- package/client/components/ai/ai-config.jsx +54 -5
- package/client/components/ai/ai.styl +73 -0
- package/client/components/batch-op/batch-op-runner.jsx +1 -2
- package/client/components/bookmark-form/common/bookmark-group-tree-format.js +1 -1
- package/client/components/bookmark-form/common/bookmark-select.jsx +1 -1
- package/client/components/bookmark-form/tree-select.jsx +1 -1
- package/client/components/main/main.jsx +3 -3
- package/client/components/rdp/file-transfer.js +3 -0
- package/client/components/setting-panel/setting-terminal.jsx +12 -0
- package/client/components/setting-panel/start-session-select.jsx +1 -1
- package/client/components/terminal/drop-file-modal.jsx +3 -3
- package/client/components/terminal/terminal-apis.js +8 -0
- package/client/components/terminal/terminal-error-handle.jsx +1 -1
- package/client/components/terminal/terminal-interactive-ui.jsx +157 -0
- package/client/components/terminal/terminal-interactive.jsx +65 -125
- package/client/components/terminal/terminal.jsx +28 -14
- package/client/components/terminal-info/base.jsx +25 -14
- package/client/components/terminal-info/log-path-edit.jsx +86 -0
- package/client/components/terminal-info/terminal-info-entry.jsx +11 -0
- package/client/components/text-editor/text-editor-entry.jsx +11 -0
- package/client/components/widgets/widget-form.jsx +30 -2
- package/client/entry/worker.js +9 -5
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal interactive UI - renders a single interactive event modal
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Form, Button } from 'antd'
|
|
6
|
+
import Modal from '../common/modal'
|
|
7
|
+
import InputAutoFocus from '../common/input-auto-focus'
|
|
8
|
+
|
|
9
|
+
const e = window.translate
|
|
10
|
+
const FormItem = Form.Item
|
|
11
|
+
|
|
12
|
+
export default function TermInteractiveUI ({
|
|
13
|
+
opts,
|
|
14
|
+
onSend,
|
|
15
|
+
onClose
|
|
16
|
+
}) {
|
|
17
|
+
const [form] = Form.useForm()
|
|
18
|
+
|
|
19
|
+
function onCancel () {
|
|
20
|
+
onSend({
|
|
21
|
+
id: opts.id,
|
|
22
|
+
results: []
|
|
23
|
+
})
|
|
24
|
+
onClose()
|
|
25
|
+
}
|
|
26
|
+
function onOk () {
|
|
27
|
+
form.submit()
|
|
28
|
+
}
|
|
29
|
+
function onConfirm () {
|
|
30
|
+
onSend({
|
|
31
|
+
id: opts.id,
|
|
32
|
+
results: [opts.options.confirmResult || 'yes']
|
|
33
|
+
})
|
|
34
|
+
onClose()
|
|
35
|
+
}
|
|
36
|
+
function onIgnore () {
|
|
37
|
+
onSend({
|
|
38
|
+
id: opts.id,
|
|
39
|
+
results: Object.keys(opts.options.prompts).map(() => '')
|
|
40
|
+
})
|
|
41
|
+
onClose()
|
|
42
|
+
}
|
|
43
|
+
function onFinish (res) {
|
|
44
|
+
onSend({
|
|
45
|
+
id: opts.id,
|
|
46
|
+
results: Object.values(res)
|
|
47
|
+
})
|
|
48
|
+
onClose()
|
|
49
|
+
}
|
|
50
|
+
function renderFormItem (pro, i) {
|
|
51
|
+
const {
|
|
52
|
+
prompt,
|
|
53
|
+
echo
|
|
54
|
+
} = pro
|
|
55
|
+
const note = (opts.options.instructions || [])[i]
|
|
56
|
+
const type = echo
|
|
57
|
+
? 'input'
|
|
58
|
+
: 'password'
|
|
59
|
+
return (
|
|
60
|
+
<FormItem
|
|
61
|
+
key={prompt + i}
|
|
62
|
+
label={prompt}
|
|
63
|
+
rules={[{
|
|
64
|
+
required: true, message: 'required'
|
|
65
|
+
}]}
|
|
66
|
+
>
|
|
67
|
+
<div>
|
|
68
|
+
<pre>{note}</pre>
|
|
69
|
+
</div>
|
|
70
|
+
<FormItem noStyle name={'item' + i}>
|
|
71
|
+
<InputAutoFocus
|
|
72
|
+
type={type}
|
|
73
|
+
placeholder={note}
|
|
74
|
+
/>
|
|
75
|
+
</FormItem>
|
|
76
|
+
</FormItem>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
function renderConfirmBody () {
|
|
80
|
+
const instructions = opts.options.instructions || []
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
{
|
|
84
|
+
instructions.map((note, index) => {
|
|
85
|
+
return <pre key={note + index}>{note}</pre>
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
<FormItem>
|
|
89
|
+
<Button
|
|
90
|
+
type='primary'
|
|
91
|
+
onClick={onConfirm}
|
|
92
|
+
>
|
|
93
|
+
{opts.options.submitText || e('submit')}
|
|
94
|
+
</Button>
|
|
95
|
+
<Button
|
|
96
|
+
className='mg1l'
|
|
97
|
+
onClick={onCancel}
|
|
98
|
+
>
|
|
99
|
+
{opts.options.cancelText || e('cancel')}
|
|
100
|
+
</Button>
|
|
101
|
+
</FormItem>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const props = {
|
|
106
|
+
maskClosable: false,
|
|
107
|
+
okText: e('submit'),
|
|
108
|
+
onCancel,
|
|
109
|
+
onOk,
|
|
110
|
+
closable: false,
|
|
111
|
+
open: true,
|
|
112
|
+
title: opts.options?.name || '?',
|
|
113
|
+
footer: null
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<Modal
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{
|
|
120
|
+
opts.options?.mode === 'confirm'
|
|
121
|
+
? renderConfirmBody()
|
|
122
|
+
: (
|
|
123
|
+
<Form
|
|
124
|
+
form={form}
|
|
125
|
+
layout='vertical'
|
|
126
|
+
onFinish={onFinish}
|
|
127
|
+
>
|
|
128
|
+
{
|
|
129
|
+
opts.options.prompts.map(renderFormItem)
|
|
130
|
+
}
|
|
131
|
+
<FormItem>
|
|
132
|
+
<Button
|
|
133
|
+
type='primary'
|
|
134
|
+
htmlType='submit'
|
|
135
|
+
>
|
|
136
|
+
{e('submit')}
|
|
137
|
+
</Button>
|
|
138
|
+
<Button
|
|
139
|
+
type='dashed'
|
|
140
|
+
className='mg1l'
|
|
141
|
+
onClick={onIgnore}
|
|
142
|
+
>
|
|
143
|
+
{e('ignore')}
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
className='mg1l'
|
|
147
|
+
onClick={onCancel}
|
|
148
|
+
>
|
|
149
|
+
{e('cancel')}
|
|
150
|
+
</Button>
|
|
151
|
+
</FormItem>
|
|
152
|
+
</Form>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
</Modal>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* handle terminal interactive operation
|
|
2
|
+
* handle terminal interactive operation - queue based
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useEffect, useState } from 'react'
|
|
6
|
-
import { Form, Button } from 'antd'
|
|
7
|
-
import Modal from '../common/modal'
|
|
8
|
-
import InputAutoFocus from '../common/input-auto-focus'
|
|
5
|
+
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
9
6
|
import wait from '../../common/wait'
|
|
10
|
-
|
|
11
|
-
const e = window.translate
|
|
12
|
-
const FormItem = Form.Item
|
|
7
|
+
import TermInteractiveUI from './terminal-interactive-ui'
|
|
13
8
|
|
|
14
9
|
export default function TermInteractive () {
|
|
15
|
-
const [
|
|
16
|
-
const
|
|
10
|
+
const [current, setCurrent] = useState(null)
|
|
11
|
+
const queueRef = useRef([])
|
|
12
|
+
const hasCurrentRef = useRef(false)
|
|
13
|
+
|
|
17
14
|
function updateTab (data) {
|
|
18
15
|
window.store.updateTab(data.tabId, data.update)
|
|
19
16
|
}
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
function processNext () {
|
|
19
|
+
const next = queueRef.current.shift()
|
|
20
|
+
if (next) {
|
|
21
|
+
setCurrent(next)
|
|
22
|
+
} else {
|
|
23
|
+
hasCurrentRef.current = false
|
|
24
|
+
setCurrent(null)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const onMsgRef = useRef(null)
|
|
29
|
+
onMsgRef.current = function onMsg (e) {
|
|
21
30
|
if (
|
|
22
31
|
e &&
|
|
23
32
|
e.data &&
|
|
24
33
|
typeof e.data === 'string' &&
|
|
25
34
|
e.data.includes('session-interactive')
|
|
26
35
|
) {
|
|
27
|
-
|
|
36
|
+
const parsed = JSON.parse(e.data)
|
|
37
|
+
if (hasCurrentRef.current) {
|
|
38
|
+
queueRef.current.push(parsed)
|
|
39
|
+
} else {
|
|
40
|
+
hasCurrentRef.current = true
|
|
41
|
+
setCurrent(parsed)
|
|
42
|
+
}
|
|
28
43
|
} else if (
|
|
29
44
|
e &&
|
|
30
45
|
e.data &&
|
|
@@ -34,127 +49,52 @@ export default function TermInteractive () {
|
|
|
34
49
|
updateTab(JSON.parse(e.data))
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
function onCancel () {
|
|
42
|
-
window.et.commonWs.s({
|
|
43
|
-
id: opts.id,
|
|
44
|
-
results: []
|
|
45
|
-
})
|
|
46
|
-
clear()
|
|
47
|
-
}
|
|
48
|
-
function onOk () {
|
|
49
|
-
form.submit()
|
|
50
|
-
}
|
|
51
|
-
function onIgnore () {
|
|
52
|
-
window.et.commonWs.s({
|
|
53
|
-
id: opts.id,
|
|
54
|
-
results: Object.keys(opts.options.prompts).map(() => '')
|
|
55
|
-
})
|
|
56
|
-
clear()
|
|
57
|
-
}
|
|
58
|
-
function onFinish (res) {
|
|
59
|
-
window.et.commonWs.s({
|
|
60
|
-
id: opts.id,
|
|
61
|
-
results: Object.values(res)
|
|
62
|
-
})
|
|
63
|
-
clear()
|
|
64
|
-
}
|
|
65
|
-
function renderFormItem (pro, i) {
|
|
66
|
-
const {
|
|
67
|
-
prompt,
|
|
68
|
-
echo
|
|
69
|
-
} = pro
|
|
70
|
-
const note = (opts.options.instructions || [])[i]
|
|
71
|
-
const type = echo
|
|
72
|
-
? 'input'
|
|
73
|
-
: 'password'
|
|
74
|
-
return (
|
|
75
|
-
<FormItem
|
|
76
|
-
key={prompt + i}
|
|
77
|
-
label={prompt}
|
|
78
|
-
rules={[{
|
|
79
|
-
required: true, message: 'required'
|
|
80
|
-
}]}
|
|
81
|
-
>
|
|
82
|
-
<div>
|
|
83
|
-
<pre>{note}</pre>
|
|
84
|
-
</div>
|
|
85
|
-
<FormItem noStyle name={'item' + i}>
|
|
86
|
-
<InputAutoFocus
|
|
87
|
-
type={type}
|
|
88
|
-
placeholder={note}
|
|
89
|
-
/>
|
|
90
|
-
</FormItem>
|
|
91
|
-
</FormItem>
|
|
92
|
-
)
|
|
52
|
+
|
|
53
|
+
function onSend (data) {
|
|
54
|
+
window.et.commonWs.s(data)
|
|
93
55
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
56
|
+
|
|
57
|
+
const onClose = useCallback(() => {
|
|
58
|
+
processNext()
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false
|
|
63
|
+
function handler (e) {
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
onMsgRef.current(e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function initWatch () {
|
|
69
|
+
for (;;) {
|
|
70
|
+
if (cancelled) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
if (window.et.commonWs) {
|
|
74
|
+
window.et.commonWs.addEventListener('message', handler)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
101
77
|
await wait(400)
|
|
102
78
|
}
|
|
103
79
|
}
|
|
104
|
-
}
|
|
105
|
-
function init () {
|
|
106
80
|
initWatch()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true
|
|
83
|
+
if (window.et.commonWs) {
|
|
84
|
+
window.et.commonWs.removeEventListener('message', handler)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
110
87
|
}, [])
|
|
111
|
-
|
|
88
|
+
|
|
89
|
+
if (!current) {
|
|
112
90
|
return null
|
|
113
91
|
}
|
|
114
|
-
|
|
115
|
-
maskClosable: false,
|
|
116
|
-
okText: e('submit'),
|
|
117
|
-
onCancel,
|
|
118
|
-
onOk,
|
|
119
|
-
closable: false,
|
|
120
|
-
open: true,
|
|
121
|
-
title: opts.options?.name || '?',
|
|
122
|
-
footer: null
|
|
123
|
-
}
|
|
92
|
+
|
|
124
93
|
return (
|
|
125
|
-
<
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
layout='vertical'
|
|
131
|
-
onFinish={onFinish}
|
|
132
|
-
>
|
|
133
|
-
{
|
|
134
|
-
opts.options.prompts.map(renderFormItem)
|
|
135
|
-
}
|
|
136
|
-
<FormItem>
|
|
137
|
-
<Button
|
|
138
|
-
type='primary'
|
|
139
|
-
htmlType='submit'
|
|
140
|
-
>
|
|
141
|
-
{e('submit')}
|
|
142
|
-
</Button>
|
|
143
|
-
<Button
|
|
144
|
-
type='dashed'
|
|
145
|
-
className='mg1l'
|
|
146
|
-
onClick={onIgnore}
|
|
147
|
-
>
|
|
148
|
-
{e('ignore')}
|
|
149
|
-
</Button>
|
|
150
|
-
<Button
|
|
151
|
-
className='mg1l'
|
|
152
|
-
onClick={onCancel}
|
|
153
|
-
>
|
|
154
|
-
{e('cancel')}
|
|
155
|
-
</Button>
|
|
156
|
-
</FormItem>
|
|
157
|
-
</Form>
|
|
158
|
-
</Modal>
|
|
94
|
+
<TermInteractiveUI
|
|
95
|
+
opts={current}
|
|
96
|
+
onSend={onSend}
|
|
97
|
+
onClose={onClose}
|
|
98
|
+
/>
|
|
159
99
|
)
|
|
160
100
|
}
|
|
@@ -70,6 +70,7 @@ class Term extends Component {
|
|
|
70
70
|
hasSelection: false,
|
|
71
71
|
saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
|
|
72
72
|
addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
|
|
73
|
+
logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
|
|
73
74
|
passType: 'password',
|
|
74
75
|
lines: [],
|
|
75
76
|
searchResults: [],
|
|
@@ -400,10 +401,15 @@ class Term extends Component {
|
|
|
400
401
|
return
|
|
401
402
|
}
|
|
402
403
|
if (isSshTerminal) {
|
|
403
|
-
this.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
405
|
+
if (behavior === 'ask') {
|
|
406
|
+
this.setState({
|
|
407
|
+
dropFileModalVisible: true,
|
|
408
|
+
droppedFiles: [{ path: filePath, isRemote: true }]
|
|
409
|
+
})
|
|
410
|
+
} else {
|
|
411
|
+
this.handleDropFileAction(behavior, [{ path: filePath, isRemote: true }])
|
|
412
|
+
}
|
|
407
413
|
return
|
|
408
414
|
}
|
|
409
415
|
this.attachAddon._sendData(`"${filePath}" `)
|
|
@@ -425,10 +431,15 @@ class Term extends Component {
|
|
|
425
431
|
}
|
|
426
432
|
|
|
427
433
|
if (isSshTerminal) {
|
|
428
|
-
this.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
434
|
+
const behavior = this.props.config.dragDropBehavior || 'ask'
|
|
435
|
+
if (behavior === 'ask') {
|
|
436
|
+
this.setState({
|
|
437
|
+
dropFileModalVisible: true,
|
|
438
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
439
|
+
})
|
|
440
|
+
} else {
|
|
441
|
+
this.handleDropFileAction(behavior, filePaths.map(path => ({ path, isRemote: false })))
|
|
442
|
+
}
|
|
432
443
|
return
|
|
433
444
|
}
|
|
434
445
|
|
|
@@ -444,8 +455,8 @@ class Term extends Component {
|
|
|
444
455
|
})
|
|
445
456
|
}
|
|
446
457
|
|
|
447
|
-
handleDropFileAction = (action) => {
|
|
448
|
-
const
|
|
458
|
+
handleDropFileAction = (action, filesOverride) => {
|
|
459
|
+
const droppedFiles = filesOverride || this.state.droppedFiles
|
|
449
460
|
if (!droppedFiles || !droppedFiles.length) {
|
|
450
461
|
this.handleDropFileModalCancel()
|
|
451
462
|
return
|
|
@@ -454,7 +465,7 @@ class Term extends Component {
|
|
|
454
465
|
const filePaths = droppedFiles.map(f => f.path)
|
|
455
466
|
|
|
456
467
|
switch (action) {
|
|
457
|
-
case '
|
|
468
|
+
case 'trz': {
|
|
458
469
|
if (this.trzszClient && this.trzszClient.isActive) {
|
|
459
470
|
message.warning('A transfer is already in progress')
|
|
460
471
|
this.handleDropFileModalCancel()
|
|
@@ -464,7 +475,7 @@ class Term extends Component {
|
|
|
464
475
|
this.attachAddon._sendData('trz\r')
|
|
465
476
|
break
|
|
466
477
|
}
|
|
467
|
-
case '
|
|
478
|
+
case 'rz':{
|
|
468
479
|
if (this.zmodemClient && this.zmodemClient.isActive) {
|
|
469
480
|
message.warning('A transfer is already in progress')
|
|
470
481
|
this.handleDropFileModalCancel()
|
|
@@ -474,7 +485,7 @@ class Term extends Component {
|
|
|
474
485
|
this.attachAddon._sendData('rz\r')
|
|
475
486
|
break
|
|
476
487
|
}
|
|
477
|
-
case '
|
|
488
|
+
case 'inputOnly':
|
|
478
489
|
default: {
|
|
479
490
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
480
491
|
this.attachAddon._sendData(filesAll)
|
|
@@ -825,6 +836,9 @@ class Term extends Component {
|
|
|
825
836
|
if (currentCmd && currentCmd.trim() && this.shouldUseManualHistory()) {
|
|
826
837
|
window.store.addCmdHistory(currentCmd.trim())
|
|
827
838
|
}
|
|
839
|
+
if (currentCmd && currentCmd.trim() === 'exit') {
|
|
840
|
+
this.userTypeExit = true
|
|
841
|
+
}
|
|
828
842
|
this.closeSuggestions()
|
|
829
843
|
}
|
|
830
844
|
}
|
|
@@ -1223,7 +1237,7 @@ class Term extends Component {
|
|
|
1223
1237
|
...extra,
|
|
1224
1238
|
...execOpts,
|
|
1225
1239
|
logName,
|
|
1226
|
-
sessionLogPath:
|
|
1240
|
+
sessionLogPath: this.state.logPath,
|
|
1227
1241
|
...pick(config, [
|
|
1228
1242
|
'addTimeStampToTermLog',
|
|
1229
1243
|
'keepaliveCountMax',
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* show base terminal info, id sessionID
|
|
3
3
|
*/
|
|
4
4
|
import { Component } from 'react'
|
|
5
|
-
import { osResolve } from '../../common/resolve'
|
|
6
5
|
import {
|
|
7
6
|
Switch,
|
|
8
7
|
Space,
|
|
9
8
|
Button
|
|
10
9
|
} from 'antd'
|
|
11
|
-
import ShowItem from '../common/show-item'
|
|
12
10
|
import defaults from '../../common/default-setting'
|
|
13
|
-
import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
|
|
11
|
+
import { toggleTerminalLog, toggleTerminalLogTimestamp, setTerminalLogPath } from '../terminal/terminal-apis'
|
|
14
12
|
import {
|
|
15
13
|
ClockCircleOutlined,
|
|
16
14
|
BorderlessTableOutlined,
|
|
@@ -19,8 +17,8 @@ import {
|
|
|
19
17
|
ApiOutlined,
|
|
20
18
|
PartitionOutlined
|
|
21
19
|
} from '@ant-design/icons'
|
|
22
|
-
import createDefaultSessionLogPath from '../../common/default-log-path'
|
|
23
20
|
import { refs } from '../common/ref'
|
|
21
|
+
import LogPathEdit from './log-path-edit'
|
|
24
22
|
|
|
25
23
|
const e = window.translate
|
|
26
24
|
|
|
@@ -36,7 +34,8 @@ const mapper = {
|
|
|
36
34
|
export default class TerminalInfoBase extends Component {
|
|
37
35
|
state = {
|
|
38
36
|
saveTerminalLogToFile: false,
|
|
39
|
-
addTimeStampToTermLog: false
|
|
37
|
+
addTimeStampToTermLog: false,
|
|
38
|
+
logPath: ''
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
componentDidMount () {
|
|
@@ -75,6 +74,17 @@ export default class TerminalInfoBase extends Component {
|
|
|
75
74
|
})
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
onLogPathChange = (v) => {
|
|
78
|
+
const { pid } = this.props
|
|
79
|
+
setTerminalLogPath(pid, v)
|
|
80
|
+
refs.get('term-' + pid)?.setState({
|
|
81
|
+
logPath: v
|
|
82
|
+
})
|
|
83
|
+
this.setState({
|
|
84
|
+
logPath: v
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
handleToggle = () => {
|
|
79
89
|
const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
|
|
80
90
|
const {
|
|
@@ -101,7 +111,8 @@ export default class TerminalInfoBase extends Component {
|
|
|
101
111
|
if (term) {
|
|
102
112
|
this.setState({
|
|
103
113
|
saveTerminalLogToFile: term.state.saveTerminalLogToFile,
|
|
104
|
-
addTimeStampToTermLog: term.state.addTimeStampToTermLog
|
|
114
|
+
addTimeStampToTermLog: term.state.addTimeStampToTermLog,
|
|
115
|
+
logPath: term.state.logPath
|
|
105
116
|
})
|
|
106
117
|
} else {
|
|
107
118
|
this.timer = setTimeout(this.getState, 100)
|
|
@@ -156,15 +167,10 @@ export default class TerminalInfoBase extends Component {
|
|
|
156
167
|
const {
|
|
157
168
|
id,
|
|
158
169
|
logName,
|
|
159
|
-
|
|
170
|
+
pid
|
|
160
171
|
} = this.props
|
|
161
|
-
const { saveTerminalLogToFile } = this.state
|
|
162
|
-
const base = sessionLogPath || createDefaultSessionLogPath()
|
|
163
|
-
const path = osResolve(base, logName + '.log')
|
|
172
|
+
const { saveTerminalLogToFile, logPath } = this.state
|
|
164
173
|
const name = e('saveTerminalLogToFile')
|
|
165
|
-
const to = saveTerminalLogToFile
|
|
166
|
-
? <ShowItem disabled={!saveTerminalLogToFile} to={path}>{path}</ShowItem>
|
|
167
|
-
: path
|
|
168
174
|
return (
|
|
169
175
|
<div className='terminal-info-section terminal-info-base'>
|
|
170
176
|
<div className='fix'>
|
|
@@ -187,7 +193,12 @@ export default class TerminalInfoBase extends Component {
|
|
|
187
193
|
this.renderInfoSelection()
|
|
188
194
|
}
|
|
189
195
|
</div>
|
|
190
|
-
<
|
|
196
|
+
<LogPathEdit
|
|
197
|
+
pid={pid}
|
|
198
|
+
logPath={logPath}
|
|
199
|
+
logName={logName}
|
|
200
|
+
setLogPath={this.onLogPathChange}
|
|
201
|
+
/>
|
|
191
202
|
</div>
|
|
192
203
|
)
|
|
193
204
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-terminal log path editor
|
|
3
|
+
* Reads/writes logPath on the terminal's own state via refs
|
|
4
|
+
*/
|
|
5
|
+
import { Button } from 'antd'
|
|
6
|
+
import message from '../common/message'
|
|
7
|
+
import InputConfirm from '../common/input-confirm'
|
|
8
|
+
import ShowItem from '../common/show-item'
|
|
9
|
+
import { chooseSaveDirectory } from '../../common/choose-save-folder'
|
|
10
|
+
import createDefaultLogPath from '../../common/default-log-path'
|
|
11
|
+
import { osResolve } from '../../common/resolve'
|
|
12
|
+
|
|
13
|
+
const e = window.translate
|
|
14
|
+
|
|
15
|
+
export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
|
|
16
|
+
const defaultPath = createDefaultLogPath()
|
|
17
|
+
const base = logPath || defaultPath
|
|
18
|
+
const fullPath = osResolve(base, logName + '.log')
|
|
19
|
+
|
|
20
|
+
const testAndSet = async (v) => {
|
|
21
|
+
if (v) {
|
|
22
|
+
try {
|
|
23
|
+
const { fs } = window
|
|
24
|
+
const uid = 'test-' + Date.now()
|
|
25
|
+
const testFile = osResolve(v, uid + '.test.log')
|
|
26
|
+
await fs.touch(testFile)
|
|
27
|
+
await fs.unlink(testFile)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.log('log path test failed', err)
|
|
30
|
+
message.error('invalid log folder')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
setLogPath(v)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleChange = (v) => {
|
|
38
|
+
testAndSet(v)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleChooseFolder = async () => {
|
|
42
|
+
const path = await chooseSaveDirectory()
|
|
43
|
+
if (path) {
|
|
44
|
+
handleChange(path)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleReset = () => {
|
|
49
|
+
setLogPath('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const inputProps = {
|
|
53
|
+
value: logPath,
|
|
54
|
+
placeholder: defaultPath,
|
|
55
|
+
onChange: handleChange,
|
|
56
|
+
addonAfter: (
|
|
57
|
+
<>
|
|
58
|
+
<Button
|
|
59
|
+
onClick={handleChooseFolder}
|
|
60
|
+
className='mg1r'
|
|
61
|
+
type='text'
|
|
62
|
+
size='small'
|
|
63
|
+
>
|
|
64
|
+
{e('chooseFolder')}
|
|
65
|
+
</Button>
|
|
66
|
+
<Button
|
|
67
|
+
size='small'
|
|
68
|
+
type='text'
|
|
69
|
+
onClick={handleReset}
|
|
70
|
+
>
|
|
71
|
+
{e('reset')}
|
|
72
|
+
</Button>
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
prefix: e('terminalLogPath') + ': '
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className='pd1b'>
|
|
80
|
+
<InputConfirm {...inputProps} />
|
|
81
|
+
<div className='pd1t font-xs color-grey'>
|
|
82
|
+
{fullPath} <ShowItem to={fullPath} />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|