@electerm/electerm-react 5.3.26 → 5.5.9
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 +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +15 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +8 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +126 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +523 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +17 -10
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +96 -0
- package/client/components/terminal/startup-queue.js +433 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +227 -2064
- package/client/components/terminal/terminal.styl +61 -0
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +29 -8
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +42 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
REMOTE_MONITOR_COMMANDS,
|
|
4
|
+
REMOTE_MONITOR_INTERVALS,
|
|
5
|
+
deriveNetworkRates,
|
|
6
|
+
getUsageLevel,
|
|
7
|
+
parseActivities,
|
|
8
|
+
parseCpu,
|
|
9
|
+
parseDisks,
|
|
10
|
+
parseMemory,
|
|
11
|
+
parseNetwork,
|
|
12
|
+
parseSysInfo,
|
|
13
|
+
parseUptime,
|
|
14
|
+
parseUsers
|
|
15
|
+
} from './monitor-model.js'
|
|
16
|
+
|
|
17
|
+
const GROUP_NAMES = Object.keys(REMOTE_MONITOR_COMMANDS)
|
|
18
|
+
const monitors = new Map()
|
|
19
|
+
|
|
20
|
+
async function executeRemoteCommand (sessionId, command) {
|
|
21
|
+
const { execCmd } = await import('../terminal/terminal-apis.js')
|
|
22
|
+
return execCmd(sessionId, command, 5000, { silent: true })
|
|
23
|
+
}
|
|
24
|
+
let subscriptionSequence = 0
|
|
25
|
+
|
|
26
|
+
function emptyGroup () {
|
|
27
|
+
return {
|
|
28
|
+
status: 'idle',
|
|
29
|
+
data: null,
|
|
30
|
+
updatedAt: null,
|
|
31
|
+
error: null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createEmptyMonitorSnapshot (sessionId = '') {
|
|
36
|
+
return {
|
|
37
|
+
sessionId,
|
|
38
|
+
groups: Object.fromEntries(GROUP_NAMES.map(name => [name, emptyGroup()])),
|
|
39
|
+
cpuHistory: [],
|
|
40
|
+
levels: { cpu: 'unknown', memory: 'unknown', disks: {} }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function unsupportedResult (result) {
|
|
45
|
+
if (!result || typeof result !== 'object') {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
const message = `${result.stderr || ''} ${result.error || ''}`
|
|
49
|
+
return result.exitCode === 126 ||
|
|
50
|
+
result.exitCode === 127 ||
|
|
51
|
+
/not found|unsupported|no such file/i.test(message)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function failedResult (result) {
|
|
55
|
+
return !result ||
|
|
56
|
+
typeof result !== 'object' ||
|
|
57
|
+
result.timedOut === true ||
|
|
58
|
+
(result.exitCode !== null && result.exitCode !== undefined && result.exitCode !== 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseGroup (name, results, sampleTimestamp, previousNetwork) {
|
|
62
|
+
if (name === 'sysinfo') {
|
|
63
|
+
return parseSysInfo(results[0], results[1])
|
|
64
|
+
}
|
|
65
|
+
const result = results[0]
|
|
66
|
+
if (name === 'cpu') return parseCpu(result)
|
|
67
|
+
if (name === 'memory') return parseMemory(result)
|
|
68
|
+
if (name === 'uptime') return parseUptime(result, sampleTimestamp)
|
|
69
|
+
if (name === 'users') return parseUsers(result)
|
|
70
|
+
if (name === 'disks') return parseDisks(result)
|
|
71
|
+
if (name === 'activities') return parseActivities(result)
|
|
72
|
+
if (name === 'network') {
|
|
73
|
+
const parsed = parseNetwork(result)
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
const current = {
|
|
78
|
+
...parsed,
|
|
79
|
+
sampleTimestamp
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
data: deriveNetworkRates(previousNetwork, current, sampleTimestamp),
|
|
83
|
+
raw: current
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class SessionMonitor {
|
|
90
|
+
constructor (sessionId, executor) {
|
|
91
|
+
this.sessionId = sessionId
|
|
92
|
+
this.executor = executor || (command => executeRemoteCommand(
|
|
93
|
+
this.sessionId,
|
|
94
|
+
command
|
|
95
|
+
))
|
|
96
|
+
this.snapshot = createEmptyMonitorSnapshot(sessionId)
|
|
97
|
+
this.subscriptions = new Map()
|
|
98
|
+
this.runners = new Map()
|
|
99
|
+
this.networkSample = null
|
|
100
|
+
this.cleanupTimer = null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
subscribe (groups, listener) {
|
|
104
|
+
const id = ++subscriptionSequence
|
|
105
|
+
clearTimeout(this.cleanupTimer)
|
|
106
|
+
this.cleanupTimer = null
|
|
107
|
+
this.subscriptions.set(id, {
|
|
108
|
+
groups: new Set(groups.filter(name => GROUP_NAMES.includes(name))),
|
|
109
|
+
listener
|
|
110
|
+
})
|
|
111
|
+
listener(this.snapshot)
|
|
112
|
+
this.reconcile()
|
|
113
|
+
return () => {
|
|
114
|
+
this.subscriptions.delete(id)
|
|
115
|
+
this.reconcile()
|
|
116
|
+
if (!this.subscriptions.size) {
|
|
117
|
+
this.cleanupTimer = setTimeout(() => {
|
|
118
|
+
if (!this.subscriptions.size) {
|
|
119
|
+
this.destroy()
|
|
120
|
+
monitors.delete(this.sessionId)
|
|
121
|
+
}
|
|
122
|
+
}, 5 * 60 * 1000)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
requestedGroups () {
|
|
128
|
+
const requested = new Set()
|
|
129
|
+
for (const subscription of this.subscriptions.values()) {
|
|
130
|
+
for (const group of subscription.groups) {
|
|
131
|
+
requested.add(group)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return requested
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
reconcile () {
|
|
138
|
+
const requested = this.requestedGroups()
|
|
139
|
+
for (const name of this.runners.keys()) {
|
|
140
|
+
if (!requested.has(name)) {
|
|
141
|
+
this.stopGroup(name)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const name of requested) {
|
|
145
|
+
this.startGroup(name)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
startGroup (name) {
|
|
150
|
+
if (this.runners.has(name)) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const current = this.snapshot.groups[name]
|
|
154
|
+
if (REMOTE_MONITOR_INTERVALS[name] === 0 && current.status === 'ready') {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (current.status === 'unsupported') {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
const runner = {
|
|
161
|
+
timer: null,
|
|
162
|
+
failures: 0
|
|
163
|
+
}
|
|
164
|
+
this.runners.set(name, runner)
|
|
165
|
+
this.runGroup(name, runner)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
stopGroup (name) {
|
|
169
|
+
const runner = this.runners.get(name)
|
|
170
|
+
if (!runner) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
clearTimeout(runner.timer)
|
|
174
|
+
this.runners.delete(name)
|
|
175
|
+
if (name === 'network') {
|
|
176
|
+
this.networkSample = null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
updateGroup (name, update) {
|
|
181
|
+
this.snapshot = {
|
|
182
|
+
...this.snapshot,
|
|
183
|
+
groups: {
|
|
184
|
+
...this.snapshot.groups,
|
|
185
|
+
[name]: {
|
|
186
|
+
...this.snapshot.groups[name],
|
|
187
|
+
...update
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.emit()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
emit () {
|
|
195
|
+
for (const subscription of this.subscriptions.values()) {
|
|
196
|
+
subscription.listener(this.snapshot)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async execute (command) {
|
|
201
|
+
return this.executor(command)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async runGroup (name, runner) {
|
|
205
|
+
if (this.runners.get(name) !== runner) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const previous = this.snapshot.groups[name]
|
|
209
|
+
if (previous.data === null || previous.data === undefined) {
|
|
210
|
+
this.updateGroup(name, {
|
|
211
|
+
status: 'loading',
|
|
212
|
+
error: null
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
let unsupported = false
|
|
216
|
+
try {
|
|
217
|
+
const commands = Array.isArray(REMOTE_MONITOR_COMMANDS[name])
|
|
218
|
+
? REMOTE_MONITOR_COMMANDS[name]
|
|
219
|
+
: [REMOTE_MONITOR_COMMANDS[name]]
|
|
220
|
+
const results = []
|
|
221
|
+
for (const command of commands) {
|
|
222
|
+
const result = await this.execute(command)
|
|
223
|
+
if (this.runners.get(name) !== runner) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (failedResult(result)) {
|
|
227
|
+
unsupported = unsupportedResult(result)
|
|
228
|
+
const error = new Error(result?.timedOut
|
|
229
|
+
? 'Remote monitor command timed out'
|
|
230
|
+
: result?.stderr || 'Remote monitor command failed')
|
|
231
|
+
error.result = result
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
234
|
+
results.push(result)
|
|
235
|
+
}
|
|
236
|
+
const sampleTimestamp = Date.now()
|
|
237
|
+
const parsed = parseGroup(name, results, sampleTimestamp, this.networkSample)
|
|
238
|
+
const data = name === 'network' ? parsed?.data : parsed
|
|
239
|
+
if (name === 'network' && parsed?.raw) {
|
|
240
|
+
this.networkSample = parsed.raw
|
|
241
|
+
}
|
|
242
|
+
if (data === null || data === undefined) {
|
|
243
|
+
throw new Error('Remote monitor response could not be parsed')
|
|
244
|
+
}
|
|
245
|
+
runner.failures = 0
|
|
246
|
+
let cpuHistory = this.snapshot.cpuHistory
|
|
247
|
+
if (name === 'cpu' && Number.isFinite(data)) {
|
|
248
|
+
cpuHistory = [
|
|
249
|
+
...cpuHistory,
|
|
250
|
+
{ timestamp: sampleTimestamp, value: data }
|
|
251
|
+
].slice(-60)
|
|
252
|
+
}
|
|
253
|
+
const levels = { ...this.snapshot.levels }
|
|
254
|
+
if (name === 'cpu') levels.cpu = getUsageLevel(data, levels.cpu)
|
|
255
|
+
if (name === 'memory') levels.memory = getUsageLevel(data.percent, levels.memory)
|
|
256
|
+
if (name === 'disks') {
|
|
257
|
+
levels.disks = Object.fromEntries(data.map(disk => [
|
|
258
|
+
disk.mount, getUsageLevel(disk.percent, levels.disks[disk.mount])
|
|
259
|
+
]))
|
|
260
|
+
}
|
|
261
|
+
this.snapshot = {
|
|
262
|
+
...this.snapshot,
|
|
263
|
+
levels,
|
|
264
|
+
cpuHistory,
|
|
265
|
+
groups: {
|
|
266
|
+
...this.snapshot.groups,
|
|
267
|
+
[name]: {
|
|
268
|
+
status: 'ready',
|
|
269
|
+
data,
|
|
270
|
+
updatedAt: sampleTimestamp,
|
|
271
|
+
error: null
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.emit()
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (this.runners.get(name) !== runner) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
runner.failures += 1
|
|
281
|
+
this.updateGroup(name, {
|
|
282
|
+
status: unsupported
|
|
283
|
+
? 'unsupported'
|
|
284
|
+
: previous.data !== null && previous.data !== undefined
|
|
285
|
+
? 'stale'
|
|
286
|
+
: 'error',
|
|
287
|
+
error: error.message || String(error)
|
|
288
|
+
})
|
|
289
|
+
if (unsupported) {
|
|
290
|
+
this.stopGroup(name)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (this.runners.get(name) !== runner) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
const interval = REMOTE_MONITOR_INTERVALS[name]
|
|
298
|
+
if (interval > 0 || runner.failures > 0) {
|
|
299
|
+
const baseInterval = interval || 5000
|
|
300
|
+
const backoff = Math.min(baseInterval * (2 ** runner.failures), 60000)
|
|
301
|
+
runner.timer = setTimeout(() => this.runGroup(name, runner), backoff)
|
|
302
|
+
} else {
|
|
303
|
+
this.runners.delete(name)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
destroy () {
|
|
308
|
+
clearTimeout(this.cleanupTimer)
|
|
309
|
+
this.cleanupTimer = null
|
|
310
|
+
for (const name of [...this.runners.keys()]) {
|
|
311
|
+
this.stopGroup(name)
|
|
312
|
+
}
|
|
313
|
+
this.subscriptions.clear()
|
|
314
|
+
this.networkSample = null
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getSessionMonitor (sessionId) {
|
|
319
|
+
let monitor = monitors.get(sessionId)
|
|
320
|
+
if (!monitor) {
|
|
321
|
+
monitor = new SessionMonitor(sessionId)
|
|
322
|
+
monitors.set(sessionId, monitor)
|
|
323
|
+
}
|
|
324
|
+
return monitor
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function subscribeRemoteMonitor (sessionId, groups, listener) {
|
|
328
|
+
return getSessionMonitor(sessionId).subscribe(groups, listener)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function documentIsVisible () {
|
|
332
|
+
return typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function useRemoteMonitor (sessionId, groups, enabled = true) {
|
|
336
|
+
const [visible, setVisible] = useState(documentIsVisible)
|
|
337
|
+
const [snapshot, setSnapshot] = useState(() => createEmptyMonitorSnapshot(sessionId))
|
|
338
|
+
const groupKey = useMemo(() => [...new Set(groups)].sort().join(','), [groups])
|
|
339
|
+
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
const onVisibilityChange = () => setVisible(documentIsVisible())
|
|
342
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
343
|
+
return () => document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
344
|
+
}, [])
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
setSnapshot(createEmptyMonitorSnapshot(sessionId))
|
|
348
|
+
if (!enabled || !visible || !sessionId || !groupKey) {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
return subscribeRemoteMonitor(
|
|
352
|
+
sessionId,
|
|
353
|
+
groupKey.split(',').filter(Boolean),
|
|
354
|
+
setSnapshot
|
|
355
|
+
)
|
|
356
|
+
}, [enabled, groupKey, sessionId, visible])
|
|
357
|
+
|
|
358
|
+
return snapshot.sessionId === sessionId
|
|
359
|
+
? snapshot
|
|
360
|
+
: createEmptyMonitorSnapshot(sessionId)
|
|
361
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { MONITOR_ITEM_GROUP } from './monitor-model'
|
|
3
|
+
import { useRemoteMonitor } from './session-monitor'
|
|
4
|
+
|
|
5
|
+
export function useMonitorDetails (sessionId, items, enabled) {
|
|
6
|
+
const itemsKey = items.join(',')
|
|
7
|
+
const groups = useMemo(() => [...new Set(items.map(id => MONITOR_ITEM_GROUP[id]).filter(Boolean))], [itemsKey])
|
|
8
|
+
const snapshot = useRemoteMonitor(sessionId, groups, enabled)
|
|
9
|
+
return { snapshot, levels: snapshot.levels }
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function getRemoteMonitorTab (store) {
|
|
2
|
+
return store.tabs.find(tab => tab.id === store.activeTabId) || store.currentTab
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isRemoteMonitorBarVisible (store) {
|
|
6
|
+
const tab = getRemoteMonitorTab(store)
|
|
7
|
+
// Read the pane explicitly so the Manate observer refreshes the global row
|
|
8
|
+
// when an SSH tab switches between terminal and SFTP views.
|
|
9
|
+
const pane = tab?.pane
|
|
10
|
+
const isSsh = tab?.host && (
|
|
11
|
+
tab.type === 'ssh' || tab.type === undefined
|
|
12
|
+
)
|
|
13
|
+
return !!store.config.remoteMonitorBarEnabled &&
|
|
14
|
+
!!isSsh &&
|
|
15
|
+
pane !== undefined &&
|
|
16
|
+
!store.showModal &&
|
|
17
|
+
!(store.rightPanelVisible && store.rightPanelTab === 'info')
|
|
18
|
+
}
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
ApartmentOutlined,
|
|
10
10
|
MoreOutlined,
|
|
11
11
|
ColumnWidthOutlined,
|
|
12
|
-
LogoutOutlined
|
|
12
|
+
LogoutOutlined,
|
|
13
|
+
PlusOutlined,
|
|
14
|
+
MinusOutlined
|
|
13
15
|
} from '@ant-design/icons'
|
|
14
16
|
import { Tooltip, Popover } from 'antd'
|
|
15
17
|
import classnames from 'classnames'
|
|
@@ -28,6 +30,7 @@ export default function SessionControl (props) {
|
|
|
28
30
|
const {
|
|
29
31
|
tab,
|
|
30
32
|
isMobile,
|
|
33
|
+
isTouchDevice,
|
|
31
34
|
isDisabled,
|
|
32
35
|
isSshDisabled,
|
|
33
36
|
isNotTerminalType,
|
|
@@ -44,7 +47,8 @@ export default function SessionControl (props) {
|
|
|
44
47
|
toggleWrap,
|
|
45
48
|
onFullscreen,
|
|
46
49
|
onOpenSearch,
|
|
47
|
-
onExitGracefully
|
|
50
|
+
onExitGracefully,
|
|
51
|
+
onZoomFontSize
|
|
48
52
|
} = props
|
|
49
53
|
|
|
50
54
|
if (isNotTerminalType) {
|
|
@@ -253,6 +257,28 @@ export default function SessionControl (props) {
|
|
|
253
257
|
)
|
|
254
258
|
}
|
|
255
259
|
|
|
260
|
+
function renderFontSizeIcons () {
|
|
261
|
+
// touch screens have no ctrl +/- shortcut to resize the terminal font,
|
|
262
|
+
// so the icons only show there — driven by store.isTouchDevice, which
|
|
263
|
+
// follows the input the user is actually using (see main.jsx).
|
|
264
|
+
if (!isTouchDevice) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
const cls = 'mg1r icon-info iblock pointer spliter font-size-control-icon'
|
|
268
|
+
return (
|
|
269
|
+
<>
|
|
270
|
+
<MinusOutlined
|
|
271
|
+
className={cls}
|
|
272
|
+
onClick={() => onZoomFontSize(-1)}
|
|
273
|
+
/>
|
|
274
|
+
<PlusOutlined
|
|
275
|
+
className={cls}
|
|
276
|
+
onClick={() => onZoomFontSize(1)}
|
|
277
|
+
/>
|
|
278
|
+
</>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
256
282
|
function renderTermControls () {
|
|
257
283
|
const { pane } = tab
|
|
258
284
|
if (pane !== paneMap.terminal) {
|
|
@@ -260,6 +286,7 @@ export default function SessionControl (props) {
|
|
|
260
286
|
}
|
|
261
287
|
return (
|
|
262
288
|
<div className='fright term-controls'>
|
|
289
|
+
{renderFontSizeIcons()}
|
|
263
290
|
{renderFullscreenIcon()}
|
|
264
291
|
{renderSearchIcon()}
|
|
265
292
|
</div>
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* terminal/sftp wrapper
|
|
3
3
|
*/
|
|
4
|
-
import { createRef } from 'react'
|
|
4
|
+
import { createRef, lazy, Suspense } from 'react'
|
|
5
5
|
import { Component } from 'manate/react/class-components'
|
|
6
6
|
import Term from '../terminal/terminal.jsx'
|
|
7
7
|
import Sftp from '../sftp/sftp-entry'
|
|
8
|
-
import RdpSession from '../rdp/rdp-session'
|
|
9
|
-
import VncSession from '../vnc/vnc-session'
|
|
10
|
-
import WebSession from '../web/web-session.jsx'
|
|
11
|
-
import SpiceSession from '../spice/spice-session'
|
|
12
8
|
import {
|
|
13
9
|
FullscreenOutlined
|
|
14
10
|
} from '@ant-design/icons'
|
|
@@ -31,8 +27,13 @@ import {
|
|
|
31
27
|
import { refs } from '../common/ref'
|
|
32
28
|
import sanitizeFilename from '../../common/sanitize-filename.js'
|
|
33
29
|
import SessionControl from './session-control'
|
|
30
|
+
import LazyBoundary from '../common/lazy-boundary'
|
|
34
31
|
import './session.styl'
|
|
35
32
|
|
|
33
|
+
const RdpSession = lazy(() => import('../rdp/rdp-session'))
|
|
34
|
+
const VncSession = lazy(() => import('../vnc/vnc-session'))
|
|
35
|
+
const WebSession = lazy(() => import('../web/web-session.jsx'))
|
|
36
|
+
const SpiceSession = lazy(() => import('../spice/spice-session'))
|
|
36
37
|
const e = window.translate
|
|
37
38
|
const SplitterPane = Splitter.Panel
|
|
38
39
|
|
|
@@ -239,9 +240,11 @@ export default class SessionWrapper extends Component {
|
|
|
239
240
|
reloadTab: this.props.reloadTab
|
|
240
241
|
}
|
|
241
242
|
return (
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
<LazyBoundary>
|
|
244
|
+
<WebSession
|
|
245
|
+
{...webProps}
|
|
246
|
+
/>
|
|
247
|
+
</LazyBoundary>
|
|
245
248
|
)
|
|
246
249
|
}
|
|
247
250
|
if (type === terminalRdpType || type === terminalVncType || type === terminalSpiceType) {
|
|
@@ -269,23 +272,29 @@ export default class SessionWrapper extends Component {
|
|
|
269
272
|
}
|
|
270
273
|
if (type === terminalVncType) {
|
|
271
274
|
return (
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
<LazyBoundary>
|
|
276
|
+
<VncSession
|
|
277
|
+
{...rdpProps}
|
|
278
|
+
/>
|
|
279
|
+
</LazyBoundary>
|
|
275
280
|
)
|
|
276
281
|
}
|
|
277
282
|
if (type === terminalSpiceType) {
|
|
278
283
|
return (
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
<LazyBoundary>
|
|
285
|
+
<SpiceSession
|
|
286
|
+
{...rdpProps}
|
|
287
|
+
/>
|
|
288
|
+
</LazyBoundary>
|
|
282
289
|
)
|
|
283
290
|
}
|
|
284
291
|
|
|
285
292
|
return (
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
<LazyBoundary>
|
|
294
|
+
<RdpSession
|
|
295
|
+
{...rdpProps}
|
|
296
|
+
/>
|
|
297
|
+
</LazyBoundary>
|
|
289
298
|
)
|
|
290
299
|
}
|
|
291
300
|
|
|
@@ -471,6 +480,15 @@ export default class SessionWrapper extends Component {
|
|
|
471
480
|
refs.get('term-' + this.props.tab.id)?.toggleSearch()
|
|
472
481
|
}
|
|
473
482
|
|
|
483
|
+
handleZoomFontSize = (v) => {
|
|
484
|
+
const term = refs.get('term-' + this.props.tab.id)
|
|
485
|
+
if (!term?.term) {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
term.zoom(v)
|
|
489
|
+
term.term.focus()
|
|
490
|
+
}
|
|
491
|
+
|
|
474
492
|
toggleWrap = () => {
|
|
475
493
|
const termRef = refs.get('term-' + this.props.tab.id)
|
|
476
494
|
if (!termRef?.term) {
|
|
@@ -598,6 +616,7 @@ export default class SessionWrapper extends Component {
|
|
|
598
616
|
<SessionControl
|
|
599
617
|
tab={this.props.tab}
|
|
600
618
|
isMobile={window.store.isMobile}
|
|
619
|
+
isTouchDevice={window.store.isTouchDevice}
|
|
601
620
|
isDisabled={this.isDisabled()}
|
|
602
621
|
isSshDisabled={this.isSshDisabled()}
|
|
603
622
|
isNotTerminalType={this.isNotTerminalType()}
|
|
@@ -613,10 +632,13 @@ export default class SessionWrapper extends Component {
|
|
|
613
632
|
toggleBroadcastInput={this.toggleBroadcastInput}
|
|
614
633
|
toggleWrap={this.toggleWrap}
|
|
615
634
|
onFullscreen={this.handleFullscreen}
|
|
635
|
+
onZoomFontSize={this.handleZoomFontSize}
|
|
616
636
|
onOpenSearch={this.handleOpenSearch}
|
|
617
637
|
onExitGracefully={this.handleExitGracefully}
|
|
618
638
|
/>
|
|
619
|
-
{
|
|
639
|
+
<Suspense fallback={null}>
|
|
640
|
+
{this.renderViews()}
|
|
641
|
+
</Suspense>
|
|
620
642
|
</div>
|
|
621
643
|
)
|
|
622
644
|
}
|
|
@@ -35,7 +35,7 @@ export default class Sessions extends Component {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
computeSessionStyle = (batch) => {
|
|
38
|
-
const style = this.props.styles[batch]
|
|
38
|
+
const style = this.props.styles[batch] || this.props.styles[0] || {}
|
|
39
39
|
return pixed(style)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -46,9 +46,12 @@ export default class Sessions extends Component {
|
|
|
46
46
|
activeTabId,
|
|
47
47
|
sizes
|
|
48
48
|
} = this.props
|
|
49
|
+
const fallbackSize = sizes[0] || { height: 0, width: 0 }
|
|
49
50
|
return tabs.map((tab) => {
|
|
50
51
|
const { id, batch } = tab
|
|
51
|
-
|
|
52
|
+
// MCP/AI-created tabs may carry an out-of-range or missing batch.
|
|
53
|
+
// Never crash the whole UI on bad data — fall back to batch 0 size.
|
|
54
|
+
const { height, width } = sizes[batch] || fallbackSize
|
|
52
55
|
const currentBatchTabId = this.props['activeTabId' + batch]
|
|
53
56
|
const cls = classNames(
|
|
54
57
|
`session-wrap session-${id}`,
|