@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,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure data, parsing and presentation helpers for the remote monitor bar.
|
|
3
|
+
* This module has no transport or UI dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ITEM_IDS = [
|
|
7
|
+
'hostname',
|
|
8
|
+
'cpu',
|
|
9
|
+
'cpuHistory',
|
|
10
|
+
'memory',
|
|
11
|
+
'upload',
|
|
12
|
+
'download',
|
|
13
|
+
'uptime',
|
|
14
|
+
'users',
|
|
15
|
+
'disks'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export const REMOTE_MONITOR_ITEM_IDS = Object.freeze([...ITEM_IDS])
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_REMOTE_MONITOR_ITEMS = Object.freeze(
|
|
21
|
+
ITEM_IDS.map(id => Object.freeze({ id, enabled: true }))
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const KNOWN_ITEM_IDS = new Set(ITEM_IDS)
|
|
25
|
+
|
|
26
|
+
function cloneDefaultItems () {
|
|
27
|
+
return DEFAULT_REMOTE_MONITOR_ITEMS.map(item => ({ ...item }))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeRemoteMonitorItems (value) {
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
return cloneDefaultItems()
|
|
33
|
+
}
|
|
34
|
+
const result = []
|
|
35
|
+
const seen = new Set()
|
|
36
|
+
for (const entry of value) {
|
|
37
|
+
const id = typeof entry === 'string' ? entry : entry && entry.id
|
|
38
|
+
if (!KNOWN_ITEM_IDS.has(id) || seen.has(id)) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const enabled = entry && typeof entry === 'object' && typeof entry.enabled === 'boolean'
|
|
42
|
+
? entry.enabled
|
|
43
|
+
: true
|
|
44
|
+
result.push({ id, enabled })
|
|
45
|
+
seen.add(id)
|
|
46
|
+
}
|
|
47
|
+
for (const id of ITEM_IDS) {
|
|
48
|
+
if (!seen.has(id)) {
|
|
49
|
+
result.push({ id, enabled: false })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const NETWORK_COMMAND = [
|
|
56
|
+
'{',
|
|
57
|
+
'[ -d /sys/class/net ] || exit 127;',
|
|
58
|
+
"default_if=$(ip route show default 2>/dev/null | awk 'NR==1 {print $5}');",
|
|
59
|
+
"printf 'default\\t%s\\n' \"$default_if\";",
|
|
60
|
+
'for d in /sys/class/net/*; do',
|
|
61
|
+
'[ -d "$d" ] || continue;',
|
|
62
|
+
'n=$' + '{d##*/};',
|
|
63
|
+
'[ "$n" = lo ] && continue;',
|
|
64
|
+
'state=$(cat "$d/operstate" 2>/dev/null || true);',
|
|
65
|
+
"ipv4=$(ip -o -4 addr show dev \"$n\" 2>/dev/null | awk 'NR==1 {print $4}');",
|
|
66
|
+
'rx=$(cat "$d/statistics/rx_bytes" 2>/dev/null || true);',
|
|
67
|
+
'tx=$(cat "$d/statistics/tx_bytes" 2>/dev/null || true);',
|
|
68
|
+
"printf 'iface\\t%s\\tstate=%s\\tipv4=%s\\trx=%s\\ttx=%s\\n' \"$n\" \"$state\" \"$ipv4\" \"$rx\" \"$tx\";",
|
|
69
|
+
'done;',
|
|
70
|
+
'}'
|
|
71
|
+
].join(' ')
|
|
72
|
+
|
|
73
|
+
export const REMOTE_MONITOR_COMMANDS = Object.freeze({
|
|
74
|
+
sysinfo: Object.freeze([
|
|
75
|
+
'uname -s -n -r -m',
|
|
76
|
+
'grep PRETTY_NAME= /etc/os-release 2>/dev/null || echo'
|
|
77
|
+
]),
|
|
78
|
+
cpu: "grep '^cpu ' /proc/stat; sleep 0.1; grep '^cpu ' /proc/stat",
|
|
79
|
+
memory: 'cat /proc/meminfo',
|
|
80
|
+
network: NETWORK_COMMAND,
|
|
81
|
+
uptime: 'cat /proc/uptime',
|
|
82
|
+
users: 'who',
|
|
83
|
+
disks: 'df -Pk',
|
|
84
|
+
activities: 'ps -eo pid=,user=,pcpu=,rss=,args= --sort=-pcpu'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export const REMOTE_MONITOR_INTERVALS = Object.freeze({
|
|
88
|
+
sysinfo: 0,
|
|
89
|
+
cpu: 5000,
|
|
90
|
+
memory: 5000,
|
|
91
|
+
network: 5000,
|
|
92
|
+
uptime: 5000,
|
|
93
|
+
users: 30000,
|
|
94
|
+
disks: 10000,
|
|
95
|
+
activities: 5000
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const REMOTE_MONITOR_COMMAND_DEFINITIONS = Object.freeze(
|
|
99
|
+
Object.fromEntries(Object.keys(REMOTE_MONITOR_COMMANDS).map(name => [
|
|
100
|
+
name,
|
|
101
|
+
Object.freeze({
|
|
102
|
+
command: REMOTE_MONITOR_COMMANDS[name],
|
|
103
|
+
interval: REMOTE_MONITOR_INTERVALS[name]
|
|
104
|
+
})
|
|
105
|
+
]))
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
function commandResult (input) {
|
|
109
|
+
if (typeof input === 'string') {
|
|
110
|
+
return { text: input, ok: true }
|
|
111
|
+
}
|
|
112
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
113
|
+
return { text: '', ok: false }
|
|
114
|
+
}
|
|
115
|
+
const text = typeof input.stdout === 'string'
|
|
116
|
+
? input.stdout
|
|
117
|
+
: typeof input.output === 'string'
|
|
118
|
+
? input.output
|
|
119
|
+
: typeof input.text === 'string'
|
|
120
|
+
? input.text
|
|
121
|
+
: ''
|
|
122
|
+
const hasFailure = input.timedOut === true || Boolean(input.error) || (
|
|
123
|
+
input.exitCode !== undefined && input.exitCode !== null && Number(input.exitCode) !== 0
|
|
124
|
+
)
|
|
125
|
+
const hasText = typeof input.stdout === 'string' || typeof input.output === 'string' || typeof input.text === 'string'
|
|
126
|
+
return { text, ok: !hasFailure && hasText }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function numeric (value) {
|
|
130
|
+
if (value === null || value === undefined || value === '' || typeof value === 'boolean') {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
const number = typeof value === 'number' ? value : Number(value)
|
|
134
|
+
return Number.isFinite(number) ? number : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function nonNegativeNumber (value) {
|
|
138
|
+
const number = numeric(value)
|
|
139
|
+
return number !== null && number >= 0 ? number : null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseCounter (value) {
|
|
143
|
+
const number = nonNegativeNumber(value)
|
|
144
|
+
return number !== null && Number.isInteger(number) ? number : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseCpuLine (line) {
|
|
148
|
+
const fields = String(line).trim().split(/\s+/)
|
|
149
|
+
if (fields.length < 3 || fields[0] !== 'cpu') {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
const values = fields.slice(1, 9).map(parseCounter)
|
|
153
|
+
return values.every(value => value !== null) ? values : null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function usageFromCpuSamples (first, second) {
|
|
157
|
+
if (!first || !second || first.length < 4 || second.length !== first.length) {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
const deltas = second.map((value, index) => value - first[index])
|
|
161
|
+
if (deltas.some(value => value < 0)) {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
const totalDelta = deltas.reduce((sum, value) => sum + value, 0)
|
|
165
|
+
if (totalDelta <= 0) {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
const idleDelta = (deltas[3] || 0) + (deltas[4] || 0)
|
|
169
|
+
const usedDelta = Math.max(0, totalDelta - idleDelta)
|
|
170
|
+
return Math.max(0, Math.min(100, usedDelta * 100 / totalDelta))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseCpu (input) {
|
|
174
|
+
const result = commandResult(input)
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
const lines = result.text.split(/\r?\n/).filter(line => /^\s*cpu(?:\s|$)/.test(line))
|
|
179
|
+
const samples = lines.map(parseCpuLine).filter(Boolean)
|
|
180
|
+
if (samples.length >= 2) {
|
|
181
|
+
return usageFromCpuSamples(samples[0], samples[1])
|
|
182
|
+
}
|
|
183
|
+
const legacy = result.text.match(/\bCPU\s+([0-9]+(?:\.[0-9]+)?)\s*%?/i)
|
|
184
|
+
if (!legacy) {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
const value = numeric(legacy[1])
|
|
188
|
+
return value === null ? null : Math.max(0, Math.min(100, value))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const MEMORY_UNITS = {
|
|
192
|
+
b: 1,
|
|
193
|
+
kb: 1024,
|
|
194
|
+
kib: 1024,
|
|
195
|
+
mb: 1024 ** 2,
|
|
196
|
+
mib: 1024 ** 2,
|
|
197
|
+
gb: 1024 ** 3,
|
|
198
|
+
gib: 1024 ** 3,
|
|
199
|
+
tb: 1024 ** 4,
|
|
200
|
+
tib: 1024 ** 4
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseMemoryValue (value, unit) {
|
|
204
|
+
const number = numeric(value)
|
|
205
|
+
if (number === null || number < 0) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
const multiplier = unit ? MEMORY_UNITS[String(unit).toLowerCase()] : 1
|
|
209
|
+
return multiplier ? number * multiplier : null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function parseMemory (input) {
|
|
213
|
+
const result = commandResult(input)
|
|
214
|
+
if (!result.ok || !result.text.trim()) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const values = {}
|
|
218
|
+
for (const line of result.text.split(/\r?\n/)) {
|
|
219
|
+
const match = line.match(/^([^:]+):\s*([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]+)?\s*$/)
|
|
220
|
+
if (!match) {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
const value = parseMemoryValue(match[2], match[3])
|
|
224
|
+
if (value !== null) {
|
|
225
|
+
values[match[1]] = value
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const totalBytes = values.MemTotal
|
|
229
|
+
if (totalBytes === undefined || totalBytes <= 0) {
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
const freeBytes = values.MemFree === undefined ? null : values.MemFree
|
|
233
|
+
const hasAvailable = values.MemAvailable !== undefined
|
|
234
|
+
const rawAvailable = hasAvailable ? values.MemAvailable : freeBytes
|
|
235
|
+
if (rawAvailable === null || rawAvailable === undefined) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
const availableBytes = Math.max(0, Math.min(totalBytes, rawAvailable))
|
|
239
|
+
const usedBytes = totalBytes - availableBytes
|
|
240
|
+
const swapTotalBytes = values.SwapTotal === undefined ? null : values.SwapTotal
|
|
241
|
+
const swapFreeBytes = values.SwapFree === undefined ? null : values.SwapFree
|
|
242
|
+
const swapUsedBytes = swapTotalBytes === null || swapFreeBytes === null
|
|
243
|
+
? null
|
|
244
|
+
: Math.max(0, swapTotalBytes - Math.min(swapTotalBytes, swapFreeBytes))
|
|
245
|
+
return {
|
|
246
|
+
totalBytes,
|
|
247
|
+
availableBytes,
|
|
248
|
+
usedBytes,
|
|
249
|
+
freeBytes,
|
|
250
|
+
swapTotalBytes,
|
|
251
|
+
swapUsedBytes,
|
|
252
|
+
percent: usedBytes * 100 / totalBytes,
|
|
253
|
+
compatibilityMode: !hasAvailable
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function parseUptime (input, sampleTimestamp) {
|
|
258
|
+
const result = commandResult(input)
|
|
259
|
+
if (!result.ok || !result.text.trim()) {
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
const match = result.text.trim().match(/^([0-9]+(?:\.[0-9]+)?)(?:\s|$)/)
|
|
263
|
+
if (!match) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
const seconds = nonNegativeNumber(match[1])
|
|
267
|
+
if (seconds === null) {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
const timestamp = numeric(sampleTimestamp !== undefined ? sampleTimestamp : input && input.sampleTimestamp)
|
|
271
|
+
return {
|
|
272
|
+
seconds,
|
|
273
|
+
bootTime: timestamp === null ? null : timestamp - seconds * 1000
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function parseSysInfo (unameInput, prettyInput) {
|
|
278
|
+
const uname = commandResult(unameInput)
|
|
279
|
+
if (!uname.ok || !uname.text.trim()) {
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
const fields = uname.text.trim().split(/\s+/)
|
|
283
|
+
if (fields.length < 4) {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
const [sysname, hostname, kernel, arch] = fields
|
|
287
|
+
const pretty = prettyInput === undefined ? { text: '', ok: true } : commandResult(prettyInput)
|
|
288
|
+
const prettyMatch = pretty.text.match(/(?:^|\n)PRETTY_NAME=([^\r\n]*)/)
|
|
289
|
+
let os = prettyMatch ? prettyMatch[1].trim() : sysname
|
|
290
|
+
if (os.startsWith('"') && os.endsWith('"')) {
|
|
291
|
+
os = os.slice(1, -1)
|
|
292
|
+
}
|
|
293
|
+
if (sysname === 'Darwin' && os === sysname) {
|
|
294
|
+
os = 'macOS'
|
|
295
|
+
}
|
|
296
|
+
return { os, sysname, hostname, kernel, arch }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isLoopback (name) {
|
|
300
|
+
return name === 'lo' || name.startsWith('lo:')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseInterfaceCounter (value) {
|
|
304
|
+
if (value === undefined || value === '') {
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
return parseCounter(value)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseNetworkLine (line) {
|
|
311
|
+
const trimmed = line.trim()
|
|
312
|
+
if (!trimmed) {
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
const fields = trimmed.split(/\s+/)
|
|
316
|
+
if (fields[0].toLowerCase() === 'iface' && fields.length >= 2) {
|
|
317
|
+
const name = fields[1]
|
|
318
|
+
const attributes = fields.slice(2)
|
|
319
|
+
const stateField = attributes.find(field => /^state=/i.test(field))
|
|
320
|
+
const ipv4Field = attributes.find(field => /^ipv4=/i.test(field))
|
|
321
|
+
const rxField = attributes.find(field => /^rx(?:_bytes)?=/i.test(field))
|
|
322
|
+
const txField = attributes.find(field => /^tx(?:_bytes)?=/i.test(field))
|
|
323
|
+
return {
|
|
324
|
+
name,
|
|
325
|
+
ipv4: ipv4Field ? (ipv4Field.slice(ipv4Field.indexOf('=') + 1).split('/')[0] || null) : null,
|
|
326
|
+
state: stateField ? stateField.slice(stateField.indexOf('=') + 1) || null : null,
|
|
327
|
+
rxBytes: rxField ? parseInterfaceCounter(rxField.slice(rxField.indexOf('=') + 1)) : null,
|
|
328
|
+
txBytes: txField ? parseInterfaceCounter(txField.slice(txField.indexOf('=') + 1)) : null
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (fields.length < 2 || /^(default|route)$/i.test(fields[0])) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
const name = fields[0]
|
|
335
|
+
const numericFields = fields.slice(1).filter(field => /^\d+$/.test(field))
|
|
336
|
+
if (numericFields.length < 2) {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
const state = fields.slice(1).find(field => /^(up|down|unknown|dormant|lowerlayerdown)$/i.test(field)) || null
|
|
340
|
+
const ipv4 = fields.slice(1).find(field => /^\d{1,3}(?:\.\d{1,3}){3}(?:\/\d{1,2})?$/.test(field))
|
|
341
|
+
return {
|
|
342
|
+
name,
|
|
343
|
+
ipv4: ipv4 ? ipv4.split('/')[0] : null,
|
|
344
|
+
state,
|
|
345
|
+
rxBytes: parseInterfaceCounter(numericFields[0]),
|
|
346
|
+
txBytes: parseInterfaceCounter(numericFields[1])
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function parseNetwork (input) {
|
|
351
|
+
const result = commandResult(input)
|
|
352
|
+
if (!result.ok || !result.text.trim()) {
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
let defaultInterface = null
|
|
356
|
+
const interfaces = []
|
|
357
|
+
const seen = new Set()
|
|
358
|
+
for (const line of result.text.split(/\r?\n/)) {
|
|
359
|
+
const trimmed = line.trim()
|
|
360
|
+
if (!trimmed) {
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
const defaultMatch = trimmed.match(/^(?:default|route)(?:\s+|=)([^\s]+)/i)
|
|
364
|
+
if (defaultMatch) {
|
|
365
|
+
defaultInterface = defaultMatch[1]
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
const parsed = parseNetworkLine(line)
|
|
369
|
+
if (!parsed || !parsed.name || seen.has(parsed.name)) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
interfaces.push(parsed)
|
|
373
|
+
seen.add(parsed.name)
|
|
374
|
+
}
|
|
375
|
+
return interfaces.length || defaultInterface ? { defaultInterface, interfaces } : null
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function interfaceByName (network, name) {
|
|
379
|
+
return network && Array.isArray(network.interfaces)
|
|
380
|
+
? network.interfaces.find(item => item.name === name) || null
|
|
381
|
+
: null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function selectPrimaryNetwork (network) {
|
|
385
|
+
if (!network || !Array.isArray(network.interfaces)) {
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
const preferred = interfaceByName(network, network.defaultInterface)
|
|
389
|
+
if (preferred && !isLoopback(preferred.name)) {
|
|
390
|
+
return preferred
|
|
391
|
+
}
|
|
392
|
+
return network.interfaces
|
|
393
|
+
.filter(item => item && item.name && !isLoopback(item.name))
|
|
394
|
+
.slice()
|
|
395
|
+
.sort((a, b) => {
|
|
396
|
+
const aActive = String(a.state || '').toLowerCase() === 'up' ? 1 : 0
|
|
397
|
+
const bActive = String(b.state || '').toLowerCase() === 'up' ? 1 : 0
|
|
398
|
+
const aComplete = Number.isFinite(a.rxBytes) && Number.isFinite(a.txBytes) ? 1 : 0
|
|
399
|
+
const bComplete = Number.isFinite(b.rxBytes) && Number.isFinite(b.txBytes) ? 1 : 0
|
|
400
|
+
return bActive - aActive || bComplete - aComplete || a.name.localeCompare(b.name)
|
|
401
|
+
})[0] || null
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function networkTimestamp (network, fallback) {
|
|
405
|
+
const value = fallback !== undefined
|
|
406
|
+
? fallback
|
|
407
|
+
: network && (network.sampleTimestamp !== undefined ? network.sampleTimestamp : network.timestampMs)
|
|
408
|
+
return numeric(value)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function cloneNetworkWithRates (network, sampleTimestamp, rates, reset, resetReason, elapsedMs) {
|
|
412
|
+
return {
|
|
413
|
+
defaultInterface: network.defaultInterface || null,
|
|
414
|
+
interfaces: network.interfaces.map(item => ({
|
|
415
|
+
...item,
|
|
416
|
+
rxRate: rates[item.name] ? rates[item.name].rxRate : null,
|
|
417
|
+
txRate: rates[item.name] ? rates[item.name].txRate : null
|
|
418
|
+
})),
|
|
419
|
+
sampleTimestamp,
|
|
420
|
+
elapsedMs: elapsedMs === undefined ? null : elapsedMs,
|
|
421
|
+
reset: Boolean(reset),
|
|
422
|
+
resetReason: resetReason || null
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function deriveNetworkRates (previous, current, sampleTimestamp) {
|
|
427
|
+
if (!current || !Array.isArray(current.interfaces)) {
|
|
428
|
+
return null
|
|
429
|
+
}
|
|
430
|
+
const timestamp = networkTimestamp(current, sampleTimestamp)
|
|
431
|
+
const rates = {}
|
|
432
|
+
const resetAll = reason => cloneNetworkWithRates(current, timestamp, rates, true, reason)
|
|
433
|
+
if (!previous || !Array.isArray(previous.interfaces)) {
|
|
434
|
+
return resetAll('initial-sample')
|
|
435
|
+
}
|
|
436
|
+
const previousTimestamp = networkTimestamp(previous)
|
|
437
|
+
if (timestamp === null || previousTimestamp === null || timestamp <= previousTimestamp) {
|
|
438
|
+
return resetAll('invalid-elapsed-time')
|
|
439
|
+
}
|
|
440
|
+
const previousPrimary = selectPrimaryNetwork(previous)
|
|
441
|
+
const currentPrimary = selectPrimaryNetwork(current)
|
|
442
|
+
if ((previousPrimary && currentPrimary && previousPrimary.name !== currentPrimary.name) ||
|
|
443
|
+
(!previousPrimary && currentPrimary) || (previousPrimary && !currentPrimary)) {
|
|
444
|
+
return resetAll('interface-changed')
|
|
445
|
+
}
|
|
446
|
+
const elapsedMs = timestamp - previousTimestamp
|
|
447
|
+
let hadRollback = false
|
|
448
|
+
for (const item of current.interfaces) {
|
|
449
|
+
const old = interfaceByName(previous, item.name)
|
|
450
|
+
if (!old || item.rxBytes === null || item.txBytes === null || old.rxBytes === null || old.txBytes === null) {
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
const rxDelta = item.rxBytes - old.rxBytes
|
|
454
|
+
const txDelta = item.txBytes - old.txBytes
|
|
455
|
+
if (rxDelta < 0 || txDelta < 0) {
|
|
456
|
+
hadRollback = true
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
459
|
+
rates[item.name] = {
|
|
460
|
+
rxRate: rxDelta * 1000 / elapsedMs,
|
|
461
|
+
txRate: txDelta * 1000 / elapsedMs
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return cloneNetworkWithRates(current, timestamp, rates, hadRollback, hadRollback ? 'counter-rollback' : null, elapsedMs)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function parseUsers (input) {
|
|
468
|
+
const result = commandResult(input)
|
|
469
|
+
if (!result.ok) {
|
|
470
|
+
return null
|
|
471
|
+
}
|
|
472
|
+
const sessions = []
|
|
473
|
+
for (const line of result.text.split(/\r?\n/)) {
|
|
474
|
+
const trimmed = line.trim()
|
|
475
|
+
if (!trimmed) {
|
|
476
|
+
continue
|
|
477
|
+
}
|
|
478
|
+
const fields = trimmed.split(/\s+/)
|
|
479
|
+
if (fields.length < 2) {
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
const sourceMatch = trimmed.match(/\(([^)]*)\)\s*$/)
|
|
483
|
+
const source = sourceMatch ? sourceMatch[1] : null
|
|
484
|
+
const withoutSource = sourceMatch ? trimmed.slice(0, sourceMatch.index).trim() : trimmed
|
|
485
|
+
const parts = withoutSource.split(/\s+/)
|
|
486
|
+
const user = parts[0]
|
|
487
|
+
const terminal = parts[1] || null
|
|
488
|
+
const loginTime = parts.slice(2).join(' ') || null
|
|
489
|
+
sessions.push({ user, terminal, loginTime, source })
|
|
490
|
+
}
|
|
491
|
+
if (result.text.trim() && !sessions.length) {
|
|
492
|
+
return null
|
|
493
|
+
}
|
|
494
|
+
const users = []
|
|
495
|
+
const seen = new Set()
|
|
496
|
+
for (const session of sessions) {
|
|
497
|
+
if (!seen.has(session.user)) {
|
|
498
|
+
users.push(session.user)
|
|
499
|
+
seen.add(session.user)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { users, sessions }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const PSEUDO_FILESYSTEMS = new Set([
|
|
506
|
+
'autofs', 'bpf', 'cgroup', 'cgroup2', 'configfs', 'debugfs', 'devpts',
|
|
507
|
+
'devtmpfs', 'fusectl', 'hugetlbfs', 'mqueue', 'nsfs', 'pstore', 'proc',
|
|
508
|
+
'ramfs', 'securityfs', 'squashfs', 'sysfs', 'tmpfs'
|
|
509
|
+
])
|
|
510
|
+
|
|
511
|
+
function decodeMount (value) {
|
|
512
|
+
return value
|
|
513
|
+
.replace(/\\040/g, ' ')
|
|
514
|
+
.replace(/\\011/g, '\t')
|
|
515
|
+
.replace(/\\134/g, '\\')
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function diskFromFields (filesystem, fields) {
|
|
519
|
+
const percentIndex = fields.findIndex(field => /^\d+%$/.test(field))
|
|
520
|
+
if (percentIndex < 3 || percentIndex === fields.length - 1) {
|
|
521
|
+
return null
|
|
522
|
+
}
|
|
523
|
+
const blocks = numeric(fields[percentIndex - 3])
|
|
524
|
+
const usedBlocks = numeric(fields[percentIndex - 2])
|
|
525
|
+
const availableBlocks = numeric(fields[percentIndex - 1])
|
|
526
|
+
const percent = numeric(fields[percentIndex].slice(0, -1))
|
|
527
|
+
const mount = decodeMount(fields.slice(percentIndex + 1).join(' '))
|
|
528
|
+
if ([blocks, usedBlocks, availableBlocks, percent].some(value => value === null) || !mount) {
|
|
529
|
+
return null
|
|
530
|
+
}
|
|
531
|
+
const filesystemName = filesystem || fields.slice(0, percentIndex - 3).join(' ')
|
|
532
|
+
if (!filesystemName) {
|
|
533
|
+
return null
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
filesystem: filesystemName,
|
|
537
|
+
totalBytes: blocks * 1024,
|
|
538
|
+
usedBytes: usedBlocks * 1024,
|
|
539
|
+
availableBytes: availableBlocks * 1024,
|
|
540
|
+
percent,
|
|
541
|
+
mount
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isRelevantDisk (disk) {
|
|
546
|
+
const fs = disk.filesystem.toLowerCase()
|
|
547
|
+
if (fs === 'overlay') {
|
|
548
|
+
return disk.mount === '/'
|
|
549
|
+
}
|
|
550
|
+
return !PSEUDO_FILESYSTEMS.has(fs)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function parseDisks (input) {
|
|
554
|
+
const result = commandResult(input)
|
|
555
|
+
if (!result.ok || !result.text.trim()) {
|
|
556
|
+
return null
|
|
557
|
+
}
|
|
558
|
+
const disks = []
|
|
559
|
+
let wrappedFilesystem = null
|
|
560
|
+
let sawHeader = false
|
|
561
|
+
for (const line of result.text.split(/\r?\n/)) {
|
|
562
|
+
if (!line.trim()) {
|
|
563
|
+
continue
|
|
564
|
+
}
|
|
565
|
+
if (/^\s*Filesystem\s+/i.test(line)) {
|
|
566
|
+
sawHeader = true
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
const fields = line.trim().split(/\s+/)
|
|
570
|
+
const percentIndex = fields.findIndex(field => /^\d+%$/.test(field))
|
|
571
|
+
let disk
|
|
572
|
+
if (/^\s/.test(line) && wrappedFilesystem && percentIndex >= 3) {
|
|
573
|
+
disk = diskFromFields(wrappedFilesystem, fields)
|
|
574
|
+
wrappedFilesystem = null
|
|
575
|
+
} else if (percentIndex >= 3) {
|
|
576
|
+
disk = diskFromFields(null, fields)
|
|
577
|
+
} else if (fields.length === 1) {
|
|
578
|
+
wrappedFilesystem = fields[0]
|
|
579
|
+
}
|
|
580
|
+
if (disk && isRelevantDisk(disk)) {
|
|
581
|
+
disks.push(disk)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return sawHeader || disks.length ? disks : null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function parseActivities (input) {
|
|
588
|
+
const result = commandResult(input)
|
|
589
|
+
if (!result.ok || !result.text.trim()) {
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
592
|
+
const activities = []
|
|
593
|
+
let sawHeader = false
|
|
594
|
+
for (const line of result.text.split(/\r?\n/)) {
|
|
595
|
+
const trimmed = line.trim()
|
|
596
|
+
if (!trimmed) {
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
if (/^pid\s+/i.test(trimmed)) {
|
|
600
|
+
sawHeader = true
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
const fields = trimmed.split(/\s+/)
|
|
604
|
+
if (fields.length < 4 || !/^\d+$/.test(fields[0])) {
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
const cpu = numeric(fields[2])
|
|
608
|
+
const rssKb = numeric(fields[3])
|
|
609
|
+
if (cpu === null || rssKb === null || rssKb < 0) {
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
activities.push({
|
|
613
|
+
pid: Number(fields[0]),
|
|
614
|
+
user: fields[1],
|
|
615
|
+
cpu,
|
|
616
|
+
memBytes: rssKb * 1024,
|
|
617
|
+
cmd: fields.slice(4).join(' ')
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
if (!activities.length && !sawHeader) {
|
|
621
|
+
return null
|
|
622
|
+
}
|
|
623
|
+
return activities.sort((a, b) => b.cpu - a.cpu || a.pid - b.pid)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function getUsageLevel (value, previous = 'normal') {
|
|
627
|
+
const number = numeric(value)
|
|
628
|
+
if (number === null || number < 0) {
|
|
629
|
+
return 'unknown'
|
|
630
|
+
}
|
|
631
|
+
const level = previous === 'critical' || previous === 'warning' || previous === 'normal'
|
|
632
|
+
? previous
|
|
633
|
+
: 'normal'
|
|
634
|
+
if (level === 'critical') {
|
|
635
|
+
if (number >= 85) return 'critical'
|
|
636
|
+
if (number >= 80) return 'warning'
|
|
637
|
+
return 'normal'
|
|
638
|
+
}
|
|
639
|
+
if (level === 'warning') {
|
|
640
|
+
if (number >= 90) return 'critical'
|
|
641
|
+
if (number >= 75) return 'warning'
|
|
642
|
+
return 'normal'
|
|
643
|
+
}
|
|
644
|
+
if (number >= 90) return 'critical'
|
|
645
|
+
if (number >= 80) return 'warning'
|
|
646
|
+
return 'normal'
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function compactNumber (value, decimals) {
|
|
650
|
+
const rounded = Number(value.toFixed(decimals))
|
|
651
|
+
return String(rounded)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function formatBytes (value) {
|
|
655
|
+
const bytes = numeric(value)
|
|
656
|
+
if (bytes === null || bytes < 0) {
|
|
657
|
+
return '—'
|
|
658
|
+
}
|
|
659
|
+
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
|
|
660
|
+
let index = 0
|
|
661
|
+
let scaled = bytes
|
|
662
|
+
while (scaled >= 1024 && index < units.length - 1) {
|
|
663
|
+
scaled /= 1024
|
|
664
|
+
index += 1
|
|
665
|
+
}
|
|
666
|
+
const decimals = index === 0 ? 0 : scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2
|
|
667
|
+
return `${compactNumber(scaled, decimals)} ${units[index]}`
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function formatRate (value) {
|
|
671
|
+
const bytes = numeric(value)
|
|
672
|
+
return bytes === null || bytes < 0 ? '—' : `${formatBytes(bytes)}/s`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function formatDuration (value) {
|
|
676
|
+
const seconds = numeric(value)
|
|
677
|
+
if (seconds === null || seconds < 0) {
|
|
678
|
+
return '—'
|
|
679
|
+
}
|
|
680
|
+
let remaining = Math.floor(seconds)
|
|
681
|
+
const days = Math.floor(remaining / 86400)
|
|
682
|
+
remaining %= 86400
|
|
683
|
+
const hours = Math.floor(remaining / 3600)
|
|
684
|
+
remaining %= 3600
|
|
685
|
+
const minutes = Math.floor(remaining / 60)
|
|
686
|
+
const secs = remaining % 60
|
|
687
|
+
if (days > 0) {
|
|
688
|
+
return `${days}d ${String(hours).padStart(2, '0')}h ${String(minutes).padStart(2, '0')}m ${String(secs).padStart(2, '0')}s`
|
|
689
|
+
}
|
|
690
|
+
if (hours > 0) {
|
|
691
|
+
return `${hours}h ${String(minutes).padStart(2, '0')}m ${String(secs).padStart(2, '0')}s`
|
|
692
|
+
}
|
|
693
|
+
if (minutes > 0) {
|
|
694
|
+
return `${minutes}m ${String(secs).padStart(2, '0')}s`
|
|
695
|
+
}
|
|
696
|
+
return `${secs}s`
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export const MONITOR_ITEM_GROUP = {
|
|
700
|
+
hostname: 'sysinfo',
|
|
701
|
+
cpu: 'cpu',
|
|
702
|
+
cpuHistory: 'cpu',
|
|
703
|
+
memory: 'memory',
|
|
704
|
+
upload: 'network',
|
|
705
|
+
download: 'network',
|
|
706
|
+
uptime: 'uptime',
|
|
707
|
+
users: 'users',
|
|
708
|
+
disks: 'disks',
|
|
709
|
+
swap: 'memory',
|
|
710
|
+
network: 'network',
|
|
711
|
+
activities: 'activities'
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const DISK_PRIORITY = ['/', '/home', '/var', '/data']
|
|
715
|
+
|
|
716
|
+
// Selectable info panel sections, in the order they are displayed.
|
|
717
|
+
// `mem` is the config id, `memory` is the monitor group id.
|
|
718
|
+
export const INFO_PANEL_ITEM_IDS = Object.freeze([
|
|
719
|
+
'uptime',
|
|
720
|
+
'cpu',
|
|
721
|
+
'mem',
|
|
722
|
+
'activities',
|
|
723
|
+
'network',
|
|
724
|
+
'disks',
|
|
725
|
+
'users'
|
|
726
|
+
])
|
|
727
|
+
|
|
728
|
+
const INFO_PANEL_ORDER = [
|
|
729
|
+
'hostname',
|
|
730
|
+
'uptime',
|
|
731
|
+
'cpu',
|
|
732
|
+
'memory',
|
|
733
|
+
'swap',
|
|
734
|
+
'activities',
|
|
735
|
+
'network',
|
|
736
|
+
'disks',
|
|
737
|
+
'users'
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
export function groupOf (snapshot, name) {
|
|
741
|
+
return snapshot.groups[name] || {
|
|
742
|
+
status: 'idle',
|
|
743
|
+
data: null,
|
|
744
|
+
updatedAt: null,
|
|
745
|
+
error: null
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function sortDisks (disks) {
|
|
750
|
+
return [...(Array.isArray(disks) ? disks : [])].sort((a, b) => {
|
|
751
|
+
const ai = DISK_PRIORITY.indexOf(a.mount)
|
|
752
|
+
const bi = DISK_PRIORITY.indexOf(b.mount)
|
|
753
|
+
const ar = ai === -1 ? DISK_PRIORITY.length : ai
|
|
754
|
+
const br = bi === -1 ? DISK_PRIORITY.length : bi
|
|
755
|
+
return ar - br || a.mount.localeCompare(b.mount)
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function getInfoPanelItems (items = []) {
|
|
760
|
+
const selected = new Set(['hostname', ...items.map(id => id === 'mem' ? 'memory' : id)])
|
|
761
|
+
if (selected.has('memory')) selected.delete('swap')
|
|
762
|
+
// Always render in the fixed order, no matter how the user toggled them
|
|
763
|
+
return INFO_PANEL_ORDER.filter(id => selected.has(id) && MONITOR_ITEM_GROUP[id])
|
|
764
|
+
}
|