@electerm/electerm-react 3.15.178 → 3.15.187
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 +2 -0
- package/client/common/default-setting.js +2 -1
- package/client/common/drop-position.js +59 -0
- package/client/components/bookmark-form/bookmark-schema.js +2 -0
- package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
- package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
- package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
- package/client/components/bookmark-form/config/serial.js +14 -0
- package/client/components/quick-commands/on-drop.js +6 -1
- package/client/components/quick-commands/quick-commands-box.jsx +18 -2
- package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
- package/client/components/quick-commands/quick-commands-list.jsx +18 -2
- package/client/components/session/session-control.jsx +30 -12
- package/client/components/session/session.jsx +5 -0
- package/client/components/setting-panel/setting-common.jsx +1 -0
- package/client/components/sftp/address-bookmark-item.jsx +15 -2
- package/client/components/sidebar/transport-ui.jsx +21 -14
- package/client/components/tabs/index.jsx +18 -3
- package/client/components/tabs/tab.jsx +45 -9
- package/client/components/tabs/tabs.styl +8 -0
- package/client/components/terminal/attach-addon-custom.js +147 -36
- package/client/components/terminal/highlight-addon.js +26 -8
- package/client/components/terminal/term-search.jsx +1 -1
- package/client/components/terminal/terminal.jsx +75 -1
- package/client/components/terminal-info/format-disks.js +45 -0
- package/client/components/terminal-info/run-cmd.jsx +2 -26
- package/client/components/tree-list/tree-list.jsx +57 -39
- package/client/css/basic.styl +14 -0
- package/client/css/mobile.styl +18 -0
- package/client/store/item.js +10 -6
- package/package.json +1 -1
|
@@ -99,6 +99,8 @@ export const serialBookmarkSchema = {
|
|
|
99
99
|
xany: z.boolean().optional().describe('XANY flow control'),
|
|
100
100
|
txLineEnding: z.enum(['\r', '\n', '\r\n']).optional().describe('TX line ending appended on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)'),
|
|
101
101
|
rxLineEnding: z.enum(['none', 'lf_to_crlf', 'cr_to_crlf']).optional().describe('RX line ending conversion: "none" (pass-through, default), "lf_to_crlf" (LF→CRLF for LF-only devices), "cr_to_crlf" (CR→CRLF for CR-only devices)'),
|
|
102
|
+
closeSequence: z.string().optional().describe('Key sequence sent to the serial port when the user clicks "exit gracefully" in the terminal controls (e.g. to cleanly exit GNU screen before disconnecting a Bluetooth serial console). Supports \\n \\t \\r \\\\ and \\xHH hex bytes, default "\\x01ky" (Ctrl+A, k, y - GNU screen kill-window confirm)'),
|
|
103
|
+
closeSequenceDelay: z.number().optional().describe('Milliseconds to wait after sending closeSequence before actually closing the port, default 500'),
|
|
102
104
|
description: z.string().optional().describe('Bookmark description')
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a dragged item should be inserted
|
|
3
|
+
* AFTER the drop target, based on which half of the
|
|
4
|
+
* target element the pointer is currently in.
|
|
5
|
+
*
|
|
6
|
+
* This is the fix for the "can not drag to the last
|
|
7
|
+
* position" bug: splitting each item into a top/bottom
|
|
8
|
+
* (or left/right) half gives a valid drop zone after the
|
|
9
|
+
* last item, so the insertion index can reach `length`
|
|
10
|
+
* (append) instead of being capped at `length - 1`.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} e native drag event (needs clientX/clientY)
|
|
13
|
+
* @param {HTMLElement} el resolved drop target element
|
|
14
|
+
* @param {boolean} horizontal true for horizontal lists (e.g. tabs)
|
|
15
|
+
* @returns {boolean} true => insert the dragged item after the target
|
|
16
|
+
*/
|
|
17
|
+
export function isDropAfterHalf (e, el, horizontal = false) {
|
|
18
|
+
if (!el) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const rect = el.getBoundingClientRect()
|
|
22
|
+
if (!rect.height && !rect.width) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
if (horizontal) {
|
|
26
|
+
return e.clientX > rect.left + rect.width / 2
|
|
27
|
+
}
|
|
28
|
+
return e.clientY > rect.top + rect.height / 2
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DROP_BEFORE_CLS = 'dnd-before'
|
|
32
|
+
const DROP_AFTER_CLS = 'dnd-after'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Show a directional drop indicator on the target element.
|
|
36
|
+
* first half => top edge (insert before)
|
|
37
|
+
* second half => bottom edge (insert after)
|
|
38
|
+
* Used together with the `.dnd-before` / `.dnd-after` CSS rules.
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement} el resolved drop target element
|
|
41
|
+
* @param {boolean} insertAfter result of isDropAfterHalf()
|
|
42
|
+
*/
|
|
43
|
+
export function setDropIndicator (el, insertAfter) {
|
|
44
|
+
if (!el) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
el.classList.remove(DROP_BEFORE_CLS, DROP_AFTER_CLS)
|
|
48
|
+
el.classList.add(insertAfter ? DROP_AFTER_CLS : DROP_BEFORE_CLS)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove any directional drop indicator from the target element.
|
|
53
|
+
*/
|
|
54
|
+
export function clearDropIndicator (el) {
|
|
55
|
+
if (!el) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
el.classList.remove(DROP_BEFORE_CLS, DROP_AFTER_CLS)
|
|
59
|
+
}
|
|
@@ -84,6 +84,8 @@ const bookmarkSchema = {
|
|
|
84
84
|
xany: 'boolean - enable XANY flow control, default is false',
|
|
85
85
|
txLineEnding: 'string - TX line ending on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)',
|
|
86
86
|
rxLineEnding: 'string - RX line ending conversion: "none" (default), "lf_to_crlf" (for LF-only devices), "cr_to_crlf" (for CR-only devices)',
|
|
87
|
+
closeSequence: 'string - key sequence sent to the serial port when the user clicks "exit gracefully" in the terminal controls, supports \\n \\t \\r \\\\ and \\xHH hex bytes, default "\\x01ky" (Ctrl+A, k, y - GNU screen kill-window confirm)',
|
|
88
|
+
closeSequenceDelay: 'number - ms to wait after sending closeSequence before closing the port, default 500',
|
|
87
89
|
runScripts: 'array - run scripts after connected ({delay,script})',
|
|
88
90
|
description: 'string - bookmark description'
|
|
89
91
|
},
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
connectionHoppingWarnKey
|
|
18
18
|
} from '../../../common/constants'
|
|
19
19
|
import { useState, useRef, useCallback } from 'react'
|
|
20
|
+
import { isDropAfterHalf } from '../../../common/drop-position'
|
|
20
21
|
import ConnectionHoppingWarningText from '../../common/connection-hopping-warning-text'
|
|
21
22
|
import * as ls from '../../../common/safe-local-storage'
|
|
22
23
|
import Modal from '../../common/modal'
|
|
@@ -49,33 +50,56 @@ export default function renderConnectionHopping (props) {
|
|
|
49
50
|
const [list, setList] = useState(formData.connectionHoppings || [])
|
|
50
51
|
const dragItem = useRef(null)
|
|
51
52
|
const dragOverItem = useRef(null)
|
|
53
|
+
// tracks whether the pointer is in the bottom half of the hovered row,
|
|
54
|
+
// so a drop can land after the target (including after the last row).
|
|
55
|
+
const dragOverAfter = useRef(false)
|
|
56
|
+
// { index, after } of the row currently hovered, or null. Drives the
|
|
57
|
+
// directional drop indicator on the antd table row via a className.
|
|
58
|
+
const [overRow, setOverRow] = useState(null)
|
|
52
59
|
|
|
53
60
|
const handleDragStart = useCallback((index) => {
|
|
54
61
|
dragItem.current = index
|
|
55
62
|
}, [])
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
// track target index + before/after half on every drag-over pass,
|
|
65
|
+
// because onDragEnter only fires once per row and cannot detect
|
|
66
|
+
// moving between the top and bottom half of the same row.
|
|
67
|
+
const handleDragOver = useCallback((e, index) => {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
const after = isDropAfterHalf(e, e.currentTarget)
|
|
58
70
|
dragOverItem.current = index
|
|
71
|
+
dragOverAfter.current = after
|
|
72
|
+
setOverRow({ index, after })
|
|
59
73
|
}, [])
|
|
60
74
|
|
|
61
75
|
const handleDragEnd = useCallback(() => {
|
|
76
|
+
setOverRow(null)
|
|
62
77
|
if (dragItem.current === null || dragOverItem.current === null) {
|
|
63
78
|
return
|
|
64
79
|
}
|
|
65
80
|
if (dragItem.current === dragOverItem.current) {
|
|
66
81
|
dragItem.current = null
|
|
67
82
|
dragOverItem.current = null
|
|
83
|
+
dragOverAfter.current = false
|
|
68
84
|
return
|
|
69
85
|
}
|
|
70
86
|
setList(old => {
|
|
71
87
|
const newList = [...old]
|
|
72
|
-
const
|
|
73
|
-
|
|
88
|
+
const fromIndex = dragItem.current
|
|
89
|
+
let insertIndex = dragOverAfter.current
|
|
90
|
+
? dragOverItem.current + 1
|
|
91
|
+
: dragOverItem.current
|
|
92
|
+
const [removed] = newList.splice(fromIndex, 1)
|
|
93
|
+
if (fromIndex < insertIndex) {
|
|
94
|
+
insertIndex = insertIndex - 1
|
|
95
|
+
}
|
|
96
|
+
newList.splice(insertIndex, 0, removed)
|
|
74
97
|
form.setFieldsValue({
|
|
75
98
|
connectionHoppings: newList
|
|
76
99
|
})
|
|
77
100
|
dragItem.current = null
|
|
78
101
|
dragOverItem.current = null
|
|
102
|
+
dragOverAfter.current = false
|
|
79
103
|
return newList
|
|
80
104
|
})
|
|
81
105
|
}, [form])
|
|
@@ -205,15 +229,17 @@ export default function renderConnectionHopping (props) {
|
|
|
205
229
|
<FormItem {...tailFormItemLayout}>
|
|
206
230
|
<Table
|
|
207
231
|
columns={cols}
|
|
208
|
-
className='mg3b'
|
|
232
|
+
className='mg3b connection-hopping-table'
|
|
209
233
|
pagination={false}
|
|
210
234
|
size='small'
|
|
211
235
|
onRow={(record, index) => ({
|
|
212
236
|
draggable: true,
|
|
237
|
+
className: overRow && overRow.index === index
|
|
238
|
+
? (overRow.after ? 'dnd-after' : 'dnd-before')
|
|
239
|
+
: '',
|
|
213
240
|
onDragStart: () => handleDragStart(index),
|
|
214
|
-
|
|
215
|
-
onDragEnd: handleDragEnd
|
|
216
|
-
onDragOver: (e) => e.preventDefault()
|
|
241
|
+
onDragOver: (e) => handleDragOver(e, index),
|
|
242
|
+
onDragEnd: handleDragEnd
|
|
217
243
|
})}
|
|
218
244
|
dataSource={list.map((d, i) => {
|
|
219
245
|
return {
|
|
@@ -32,10 +32,18 @@ export default function SshTunnelForm (props) {
|
|
|
32
32
|
} = props
|
|
33
33
|
|
|
34
34
|
const [isDynamic, setIsDynamic] = useState(
|
|
35
|
-
(initialValues?.sshTunnel || '
|
|
35
|
+
(initialValues?.sshTunnel || 'forwardLocalToRemote') === 'dynamicForward'
|
|
36
36
|
)
|
|
37
37
|
const [dirty, setDirty] = useState(false)
|
|
38
38
|
|
|
39
|
+
// watch host/port fields so the tooltip examples reflect what the user typed
|
|
40
|
+
const localHost = Form.useWatch('sshTunnelLocalHost', formChild) || '127.0.0.1'
|
|
41
|
+
const localPort = Form.useWatch('sshTunnelLocalPort', formChild) || '12200'
|
|
42
|
+
const remoteHost = Form.useWatch('sshTunnelRemoteHost', formChild) || '127.0.0.1'
|
|
43
|
+
const remotePort = Form.useWatch('sshTunnelRemotePort', formChild) || '12300'
|
|
44
|
+
const localAddr = `${localHost}:${localPort}`
|
|
45
|
+
const remoteAddr = `${remoteHost}:${remotePort}`
|
|
46
|
+
|
|
39
47
|
function onChange (ev) {
|
|
40
48
|
setIsDynamic(ev.target.value === 'dynamicForward')
|
|
41
49
|
}
|
|
@@ -63,13 +71,23 @@ export default function SshTunnelForm (props) {
|
|
|
63
71
|
<div>
|
|
64
72
|
<p>{e(direction)}</p>
|
|
65
73
|
<p><UserOutlined /> → {middle} → {last}</p>
|
|
74
|
+
{localToRemote
|
|
75
|
+
? (
|
|
76
|
+
<p>Connect to <b>{localAddr}</b> on your machine to reach <b>{remoteAddr}</b> on the remote server (e.g. open <b>http://{localAddr}</b> in a browser).</p>
|
|
77
|
+
)
|
|
78
|
+
: (
|
|
79
|
+
<p>Connect to <b>{remoteAddr}</b> on the remote server to reach <b>{localAddr}</b> on your machine.</p>
|
|
80
|
+
)}
|
|
66
81
|
</div>
|
|
67
82
|
)
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
function renderDynamicForward () {
|
|
71
86
|
return (
|
|
72
|
-
<
|
|
87
|
+
<div>
|
|
88
|
+
<p><UserOutlined /> → socks proxy → url</p>
|
|
89
|
+
<p>Point your app's SOCKS5 proxy at <b>{localAddr}</b> to reach any host through the remote server.</p>
|
|
90
|
+
</div>
|
|
73
91
|
)
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -125,16 +143,16 @@ export default function SshTunnelForm (props) {
|
|
|
125
143
|
required
|
|
126
144
|
>
|
|
127
145
|
<RadioGroup onChange={onChange}>
|
|
128
|
-
<RadioButton value='forwardRemoteToLocal'>
|
|
129
|
-
<Tooltip title={renderSshTunnelFlow('remoteToLocal')}>
|
|
130
|
-
<span>R→L <QuestionCircleOutlined /></span>
|
|
131
|
-
</Tooltip>
|
|
132
|
-
</RadioButton>
|
|
133
146
|
<RadioButton value='forwardLocalToRemote'>
|
|
134
147
|
<Tooltip title={renderSshTunnelFlow('localToRemote')}>
|
|
135
148
|
<span>L→R <QuestionCircleOutlined /></span>
|
|
136
149
|
</Tooltip>
|
|
137
150
|
</RadioButton>
|
|
151
|
+
<RadioButton value='forwardRemoteToLocal'>
|
|
152
|
+
<Tooltip title={renderSshTunnelFlow('remoteToLocal')}>
|
|
153
|
+
<span>R→L <QuestionCircleOutlined /></span>
|
|
154
|
+
</Tooltip>
|
|
155
|
+
</RadioButton>
|
|
138
156
|
<RadioButton value='dynamicForward'>
|
|
139
157
|
<Tooltip title={renderDynamicForward()}>
|
|
140
158
|
<span>{e('dynamicForward')}(socks proxy) <QuestionCircleOutlined /></span>
|
|
@@ -14,7 +14,7 @@ const FormItem = Form.Item
|
|
|
14
14
|
const e = window.translate
|
|
15
15
|
|
|
16
16
|
const defaultInitialValues = {
|
|
17
|
-
sshTunnel: '
|
|
17
|
+
sshTunnel: 'forwardLocalToRemote',
|
|
18
18
|
sshTunnelLocalPort: 12200,
|
|
19
19
|
sshTunnelLocalHost: '127.0.0.1',
|
|
20
20
|
sshTunnelRemotePort: 12300,
|
|
@@ -20,6 +20,8 @@ const serialConfig = {
|
|
|
20
20
|
xon: false,
|
|
21
21
|
xoff: false,
|
|
22
22
|
xany: false,
|
|
23
|
+
closeSequence: '\\x01ky',
|
|
24
|
+
closeSequenceDelay: 500,
|
|
23
25
|
term: defaultSettings.terminalType,
|
|
24
26
|
displayRaw: false,
|
|
25
27
|
runScripts: [{}],
|
|
@@ -59,6 +61,18 @@ const serialConfig = {
|
|
|
59
61
|
{ type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
|
|
60
62
|
{ type: 'select', name: 'txLineEnding', label: 'txLineEnding', options: commonTxLineEndings.map(d => ({ value: d.value, label: d.label })) },
|
|
61
63
|
{ type: 'select', name: 'rxLineEnding', label: 'rxLineEnding', options: commonRxLineEndings.map(d => ({ value: d.value, label: d.label })) },
|
|
64
|
+
{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'closeSequence',
|
|
67
|
+
label: e('closeSequence'),
|
|
68
|
+
props: { placeholder: '\\x01ky' }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'number',
|
|
72
|
+
name: 'closeSequenceDelay',
|
|
73
|
+
label: e('closeSequenceDelay'),
|
|
74
|
+
props: { min: 0, max: 10000, step: 100 }
|
|
75
|
+
},
|
|
62
76
|
commonFields.runScripts,
|
|
63
77
|
commonFields.description,
|
|
64
78
|
{ type: 'input', name: 'type', label: 'type', hidden: true }
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { isDropAfterHalf } from '../../common/drop-position'
|
|
2
|
+
|
|
1
3
|
export default function onDrop (e, cls) {
|
|
2
4
|
e.preventDefault()
|
|
3
5
|
const { store } = window
|
|
4
6
|
const idDragged = e.dataTransfer.getData('idDragged')
|
|
5
7
|
const tar = cls ? e.target.closest(cls) : e.target
|
|
6
8
|
const idDrop = tar.getAttribute('data-id')
|
|
7
|
-
|
|
9
|
+
// drop on the bottom half of the target => insert after it,
|
|
10
|
+
// which makes dropping on the last item append to the end.
|
|
11
|
+
const insertAfter = isDropAfterHalf(e, tar)
|
|
12
|
+
store.adjustOrder('quickCommands', idDragged, idDrop, insertAfter)
|
|
8
13
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '@ant-design/icons'
|
|
16
16
|
import classNames from 'classnames'
|
|
17
17
|
import onDropFunc from './on-drop'
|
|
18
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
18
19
|
import './qm.styl'
|
|
19
20
|
|
|
20
21
|
const e = window.translate
|
|
@@ -81,6 +82,10 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
81
82
|
|
|
82
83
|
function onDragOver (e) {
|
|
83
84
|
e.preventDefault()
|
|
85
|
+
const el = e.target.closest('.qm-item')
|
|
86
|
+
if (el) {
|
|
87
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
function onDragStart (e) {
|
|
@@ -88,14 +93,25 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
function onDragEnter (e) {
|
|
91
|
-
e.
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
const el = e.target.closest('.qm-item')
|
|
98
|
+
if (el) {
|
|
99
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
function onDragLeave (e) {
|
|
95
|
-
e.target.closest('.qm-item')
|
|
104
|
+
const el = e.target.closest('.qm-item')
|
|
105
|
+
if (el) {
|
|
106
|
+
clearDropIndicator(el)
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
function onDrop (e) {
|
|
111
|
+
const el = e.target.closest('.qm-item')
|
|
112
|
+
if (el) {
|
|
113
|
+
clearDropIndicator(el)
|
|
114
|
+
}
|
|
99
115
|
onDropFunc(e, '.qm-item')
|
|
100
116
|
}
|
|
101
117
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { MinusCircleOutlined, PlusOutlined, HolderOutlined } from '@ant-design/icons'
|
|
9
9
|
import HelpIcon from '../common/help-icon'
|
|
10
10
|
import { copy } from '../../common/clipboard'
|
|
11
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
11
12
|
import { useRef } from 'react'
|
|
12
13
|
|
|
13
14
|
const FormItem = Form.Item
|
|
@@ -30,28 +31,38 @@ export default function renderQm (form) {
|
|
|
30
31
|
e.dataTransfer.dropEffect = 'move'
|
|
31
32
|
const el = e.target.closest('.ant-space-compact')
|
|
32
33
|
if (dragIndexRef.current !== index && el) {
|
|
33
|
-
el
|
|
34
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function handleDragLeave (e) {
|
|
38
|
-
e.target.closest('.ant-space-compact')
|
|
39
|
+
const el = e.target.closest('.ant-space-compact')
|
|
40
|
+
if (el) {
|
|
41
|
+
clearDropIndicator(el)
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
function handleDrop (e, index, form) {
|
|
42
46
|
e.preventDefault()
|
|
43
47
|
const el = e.target.closest('.ant-space-compact')
|
|
44
|
-
el
|
|
48
|
+
clearDropIndicator(el)
|
|
45
49
|
const dragIndex = dragIndexRef.current
|
|
46
50
|
if (dragIndex === null || dragIndex === index) {
|
|
47
51
|
dragIndexRef.current = null
|
|
48
52
|
return
|
|
49
53
|
}
|
|
54
|
+
// bottom half of the row => insert after it, so the last
|
|
55
|
+
// sub-command can receive a drop (append to the end).
|
|
56
|
+
const insertAfter = isDropAfterHalf(e, el)
|
|
50
57
|
const commands = form.getFieldValue('commands') || []
|
|
51
58
|
const item = commands[dragIndex]
|
|
52
59
|
const newCommands = [...commands]
|
|
53
60
|
newCommands.splice(dragIndex, 1)
|
|
54
|
-
|
|
61
|
+
let insertIndex = insertAfter ? index + 1 : index
|
|
62
|
+
if (dragIndex < insertIndex) {
|
|
63
|
+
insertIndex = insertIndex - 1
|
|
64
|
+
}
|
|
65
|
+
newCommands.splice(insertIndex, 0, item)
|
|
55
66
|
form.setFieldValue('commands', newCommands)
|
|
56
67
|
dragIndexRef.current = null
|
|
57
68
|
}
|
|
@@ -9,6 +9,7 @@ import classnames from 'classnames'
|
|
|
9
9
|
import highlight from '../common/highlight'
|
|
10
10
|
import QmTransport from './quick-command-transport'
|
|
11
11
|
import onDrop from './on-drop'
|
|
12
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
12
13
|
import copy from 'json-deep-copy'
|
|
13
14
|
import uid from '../../common/uid'
|
|
14
15
|
|
|
@@ -37,6 +38,10 @@ export default class QuickCommandsList extends List {
|
|
|
37
38
|
|
|
38
39
|
handleDragOver = e => {
|
|
39
40
|
e.preventDefault()
|
|
41
|
+
const el = e.target.closest('.item-list-unit')
|
|
42
|
+
if (el) {
|
|
43
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
handleDragStart = e => {
|
|
@@ -47,14 +52,25 @@ export default class QuickCommandsList extends List {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
handleDragEnter = e => {
|
|
50
|
-
e.
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
const el = e.target.closest('.item-list-unit')
|
|
57
|
+
if (el) {
|
|
58
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
handleDragLeave = e => {
|
|
54
|
-
e.target.closest('.item-list-unit')
|
|
63
|
+
const el = e.target.closest('.item-list-unit')
|
|
64
|
+
if (el) {
|
|
65
|
+
clearDropIndicator(el)
|
|
66
|
+
}
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
handleDrop = e => {
|
|
70
|
+
const el = e.target.closest('.item-list-unit')
|
|
71
|
+
if (el) {
|
|
72
|
+
clearDropIndicator(el)
|
|
73
|
+
}
|
|
58
74
|
onDrop(e, '.item-list-unit')
|
|
59
75
|
}
|
|
60
76
|
|
|
@@ -9,13 +9,15 @@ import {
|
|
|
9
9
|
CloseOutlined,
|
|
10
10
|
ApartmentOutlined,
|
|
11
11
|
MoreOutlined,
|
|
12
|
-
ColumnWidthOutlined
|
|
12
|
+
ColumnWidthOutlined,
|
|
13
|
+
LogoutOutlined
|
|
13
14
|
} from '@ant-design/icons'
|
|
14
15
|
import { Tooltip, Popover } from 'antd'
|
|
15
16
|
import classnames from 'classnames'
|
|
16
17
|
import {
|
|
17
18
|
paneMap,
|
|
18
|
-
connectionMap
|
|
19
|
+
connectionMap,
|
|
20
|
+
terminalSerialType
|
|
19
21
|
} from '../../common/constants'
|
|
20
22
|
import { SplitViewIcon } from '../icons/split-view'
|
|
21
23
|
import { HeartbeatIcon } from '../icons/heartbeat'
|
|
@@ -45,13 +47,18 @@ export default function SessionControl (props) {
|
|
|
45
47
|
toggleWrap,
|
|
46
48
|
onFullscreen,
|
|
47
49
|
onOpenSearch,
|
|
48
|
-
onDismissDelKeyTip
|
|
50
|
+
onDismissDelKeyTip,
|
|
51
|
+
onExitGracefully
|
|
49
52
|
} = props
|
|
50
53
|
|
|
51
54
|
if (isNotTerminalType) {
|
|
52
55
|
return null
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
const isSsh = !!tab.authType
|
|
59
|
+
const isLocal = !isSsh && (tab.type === connectionMap.local || !tab.type)
|
|
60
|
+
const showSshFeatures = isSsh || isLocal
|
|
61
|
+
|
|
55
62
|
// ---- sub-renderers ----
|
|
56
63
|
|
|
57
64
|
function renderPaneControl () {
|
|
@@ -62,9 +69,6 @@ export default function SessionControl (props) {
|
|
|
62
69
|
if (sshSftpSplitView && canSplitView) {
|
|
63
70
|
return null
|
|
64
71
|
}
|
|
65
|
-
const termType = tab?.type
|
|
66
|
-
const isSsh = tab.authType
|
|
67
|
-
const isLocal = !isSsh && (termType === connectionMap.local || !termType)
|
|
68
72
|
const types = [
|
|
69
73
|
paneMap.terminal,
|
|
70
74
|
paneMap.fileManager
|
|
@@ -124,9 +128,6 @@ export default function SessionControl (props) {
|
|
|
124
128
|
return null
|
|
125
129
|
}
|
|
126
130
|
const { pane, enableSsh, sshSftpSplitView } = tab
|
|
127
|
-
const termType = tab?.type
|
|
128
|
-
const isSsh = tab.authType
|
|
129
|
-
const isLocal = !isSsh && (termType === connectionMap.local || !termType)
|
|
130
131
|
const checkTxt = e('sftpPathFollowSsh')
|
|
131
132
|
const checkProps = {
|
|
132
133
|
onClick: toggleCheckSftpPathFollowSsh,
|
|
@@ -161,7 +162,7 @@ export default function SessionControl (props) {
|
|
|
161
162
|
if (isMobile) {
|
|
162
163
|
return null
|
|
163
164
|
}
|
|
164
|
-
if (!canSplitView || isNotTerminalType) {
|
|
165
|
+
if (!canSplitView || isNotTerminalType || !showSshFeatures) {
|
|
165
166
|
return null
|
|
166
167
|
}
|
|
167
168
|
const title = e('sshSftpSplitView')
|
|
@@ -185,7 +186,7 @@ export default function SessionControl (props) {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
function renderKeepaliveIcon () {
|
|
188
|
-
if (isSshDisabled) {
|
|
189
|
+
if (isSshDisabled || !showSshFeatures) {
|
|
189
190
|
return null
|
|
190
191
|
}
|
|
191
192
|
const title = e('keepalive')
|
|
@@ -203,7 +204,7 @@ export default function SessionControl (props) {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
function renderBroadcastIcon () {
|
|
206
|
-
if (isSshDisabled) {
|
|
207
|
+
if (isSshDisabled || !showSshFeatures) {
|
|
207
208
|
return null
|
|
208
209
|
}
|
|
209
210
|
const title = e('broadcastInput')
|
|
@@ -235,6 +236,21 @@ export default function SessionControl (props) {
|
|
|
235
236
|
)
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
function renderExitGracefullyIcon () {
|
|
240
|
+
if (tab.type !== terminalSerialType) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
const title = e('exitGracefully')
|
|
244
|
+
return (
|
|
245
|
+
<Tooltip title={title}>
|
|
246
|
+
<LogoutOutlined
|
|
247
|
+
className='sess-icon pointer exit-gracefully-icon'
|
|
248
|
+
onClick={onExitGracefully}
|
|
249
|
+
/>
|
|
250
|
+
</Tooltip>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
function renderSearchIcon () {
|
|
239
255
|
const title = e('search')
|
|
240
256
|
return (
|
|
@@ -281,6 +297,7 @@ export default function SessionControl (props) {
|
|
|
281
297
|
{renderKeepaliveIcon()}
|
|
282
298
|
{renderBroadcastIcon()}
|
|
283
299
|
{renderWrapIcon()}
|
|
300
|
+
{renderExitGracefullyIcon()}
|
|
284
301
|
{renderTermControls()}
|
|
285
302
|
</div>
|
|
286
303
|
)
|
|
@@ -307,6 +324,7 @@ export default function SessionControl (props) {
|
|
|
307
324
|
{renderKeepaliveIcon()}
|
|
308
325
|
{renderBroadcastIcon()}
|
|
309
326
|
{renderWrapIcon()}
|
|
327
|
+
{renderExitGracefullyIcon()}
|
|
310
328
|
{renderTermControls()}
|
|
311
329
|
</div>
|
|
312
330
|
)
|
|
@@ -490,6 +490,10 @@ export default class SessionWrapper extends Component {
|
|
|
490
490
|
this.setState({ keepaliveEnabled: enabled })
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
handleExitGracefully = () => {
|
|
494
|
+
refs.get('term-' + this.props.tab.id)?.exitGracefully()
|
|
495
|
+
}
|
|
496
|
+
|
|
493
497
|
handleOpenSearch = () => {
|
|
494
498
|
refs.get('term-' + this.props.tab.id)?.toggleSearch()
|
|
495
499
|
}
|
|
@@ -640,6 +644,7 @@ export default class SessionWrapper extends Component {
|
|
|
640
644
|
onFullscreen={this.handleFullscreen}
|
|
641
645
|
onOpenSearch={this.handleOpenSearch}
|
|
642
646
|
onDismissDelKeyTip={this.handleDismissDelKeyTip}
|
|
647
|
+
onExitGracefully={this.handleExitGracefully}
|
|
643
648
|
/>
|
|
644
649
|
{this.renderViews()}
|
|
645
650
|
</div>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
Tag
|
|
7
7
|
} from 'antd'
|
|
8
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
8
9
|
|
|
9
10
|
export default class AddrBookmarkItem extends Component {
|
|
10
11
|
handleClick = () => {
|
|
@@ -31,6 +32,15 @@ export default class AddrBookmarkItem extends Component {
|
|
|
31
32
|
|
|
32
33
|
handleDragOver = e => {
|
|
33
34
|
e.preventDefault()
|
|
35
|
+
// only the hovered item should show an indicator
|
|
36
|
+
document.querySelectorAll(
|
|
37
|
+
'.addr-bookmark-item.dnd-before, .addr-bookmark-item.dnd-after'
|
|
38
|
+
).forEach(el => clearDropIndicator(el))
|
|
39
|
+
setDropIndicator(e.currentTarget, isDropAfterHalf(e, e.currentTarget))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handleDragLeave = e => {
|
|
43
|
+
clearDropIndicator(e.currentTarget)
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
handleDragStart = e => {
|
|
@@ -39,13 +49,15 @@ export default class AddrBookmarkItem extends Component {
|
|
|
39
49
|
|
|
40
50
|
handleDrop = e => {
|
|
41
51
|
e.preventDefault()
|
|
52
|
+
clearDropIndicator(e.currentTarget)
|
|
42
53
|
const { store } = window
|
|
43
54
|
const [host, idDragged] = e.dataTransfer.getData('idDragged').split('#')
|
|
44
|
-
const idDrop = e.
|
|
55
|
+
const idDrop = e.currentTarget.getAttribute('data-id').split('#')[1]
|
|
45
56
|
const dataName = host
|
|
46
57
|
? 'addressBookmarks'
|
|
47
58
|
: 'addressBookmarksLocal'
|
|
48
|
-
|
|
59
|
+
const insertAfter = isDropAfterHalf(e, e.currentTarget)
|
|
60
|
+
store.adjustOrder(dataName, idDragged, idDrop, insertAfter)
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
render () {
|
|
@@ -64,6 +76,7 @@ export default class AddrBookmarkItem extends Component {
|
|
|
64
76
|
data-id={id}
|
|
65
77
|
draggable
|
|
66
78
|
onDragOver={this.handleDragOver}
|
|
79
|
+
onDragLeave={this.handleDragLeave}
|
|
67
80
|
onDragStart={this.handleDragStart}
|
|
68
81
|
onDrop={this.handleDrop}
|
|
69
82
|
>
|