@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.
Files changed (146) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +15 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/cmd-history.jsx +132 -45
  33. package/client/components/footer/cmd-history.styl +42 -4
  34. package/client/components/footer/footer-entry.jsx +35 -5
  35. package/client/components/footer/footer.styl +8 -1
  36. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  37. package/client/components/footer/tab-select.jsx +34 -17
  38. package/client/components/layout/layout.jsx +23 -31
  39. package/client/components/layout/pixed.js +3 -0
  40. package/client/components/main/main.jsx +55 -28
  41. package/client/components/main/term-fullscreen.styl +9 -3
  42. package/client/components/quick-commands/qm.styl +3 -2
  43. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  44. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  45. package/client/components/rdp/file-transfer.js +4 -4
  46. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  47. package/client/components/remote-monitor/monitor-details.styl +87 -0
  48. package/client/components/remote-monitor/monitor-model.js +764 -0
  49. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  50. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  51. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  52. package/client/components/remote-monitor/session-monitor.js +361 -0
  53. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  54. package/client/components/remote-monitor/visibility.js +18 -0
  55. package/client/components/session/session-control.jsx +29 -2
  56. package/client/components/session/session.jsx +40 -18
  57. package/client/components/session/sessions.jsx +5 -2
  58. package/client/components/setting-panel/setting-common.jsx +25 -56
  59. package/client/components/setting-panel/setting-modal.jsx +57 -42
  60. package/client/components/setting-panel/setting-terminal.jsx +8 -2
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/sftp/sftp-entry.jsx +4 -0
  67. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  68. package/client/components/sidebar/side-panel.jsx +1 -1
  69. package/client/components/sys-menu/menu-btn.jsx +30 -1
  70. package/client/components/tabs/index.jsx +2 -2
  71. package/client/components/tabs/tab-title.jsx +3 -2
  72. package/client/components/tabs/tab.jsx +5 -1
  73. package/client/components/terminal/attach-addon-custom.js +295 -49
  74. package/client/components/terminal/automation/index.js +64 -0
  75. package/client/components/terminal/automation/keys.js +49 -0
  76. package/client/components/terminal/automation/stream-matcher.js +82 -0
  77. package/client/components/terminal/automation/strip-ansi.js +19 -0
  78. package/client/components/terminal/automation/trigger-engine.js +233 -0
  79. package/client/components/terminal/automation/trigger-presets.js +110 -0
  80. package/client/components/terminal/command-tracker-addon.js +31 -1
  81. package/client/components/terminal/mixins/index.js +27 -0
  82. package/client/components/terminal/mixins/term-attach.js +123 -0
  83. package/client/components/terminal/mixins/term-config.jsx +84 -0
  84. package/client/components/terminal/mixins/term-content.js +126 -0
  85. package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
  86. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  87. package/client/components/terminal/mixins/term-init.js +122 -0
  88. package/client/components/terminal/mixins/term-log.jsx +96 -0
  89. package/client/components/terminal/mixins/term-resize.js +87 -0
  90. package/client/components/terminal/mixins/term-socket.js +433 -0
  91. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  92. package/client/components/terminal/mixins/term-theme.js +190 -0
  93. package/client/components/terminal/mixins/term-touch.js +523 -0
  94. package/client/components/terminal/shell-detect.js +37 -0
  95. package/client/components/terminal/shell.js +0 -28
  96. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  97. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  98. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  99. package/client/components/terminal/shortcut-bar.jsx +17 -10
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +96 -0
  102. package/client/components/terminal/startup-queue.js +433 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal-select-text.jsx +69 -0
  105. package/client/components/terminal/terminal.jsx +227 -2064
  106. package/client/components/terminal/terminal.styl +61 -0
  107. package/client/components/terminal/transfer-client-base.js +5 -29
  108. package/client/components/terminal/trzsz-client.js +1 -3
  109. package/client/components/terminal/xmodem-client.js +1 -3
  110. package/client/components/terminal/xterm-loader.js +7 -0
  111. package/client/components/terminal/zmodem-client.js +1 -3
  112. package/client/components/terminal-info/base.jsx +18 -70
  113. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  114. package/client/components/terminal-info/terminal-info.jsx +50 -59
  115. package/client/components/terminal-info/terminal-info.styl +6 -49
  116. package/client/components/text-editor/open-text-editor.js +30 -0
  117. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  118. package/client/components/text-editor/text-editor.jsx +14 -0
  119. package/client/components/triggers/trigger-editor.jsx +246 -0
  120. package/client/components/triggers/trigger-form.jsx +200 -0
  121. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  122. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  123. package/client/components/triggers/trigger-transport.jsx +60 -0
  124. package/client/components/triggers/triggers-field.jsx +19 -0
  125. package/client/components/widgets/widgets-list.jsx +11 -1
  126. package/client/css/mobile.styl +29 -8
  127. package/client/store/common.js +12 -1
  128. package/client/store/init-state.js +7 -0
  129. package/client/store/mcp-handler.js +158 -30
  130. package/client/store/setting.js +9 -0
  131. package/client/store/store.js +2 -0
  132. package/client/store/sync.js +2 -0
  133. package/client/store/tab.js +42 -3
  134. package/client/store/trigger.js +118 -0
  135. package/client/store/watch.js +13 -5
  136. package/client/views/index.pug +3 -0
  137. package/package.json +1 -1
  138. package/client/components/terminal-info/activity.jsx +0 -57
  139. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  140. package/client/components/terminal-info/disk.jsx +0 -45
  141. package/client/components/terminal-info/format-disks.js +0 -45
  142. package/client/components/terminal-info/network.jsx +0 -121
  143. package/client/components/terminal-info/resource.jsx +0 -99
  144. package/client/components/terminal-info/run-cmd.jsx +0 -263
  145. package/client/components/terminal-info/sys-info.jsx +0 -56
  146. package/client/components/terminal-info/up.jsx +0 -17
@@ -0,0 +1,458 @@
1
+ import { auto } from 'manate/react'
2
+ import { Button, Popconfirm, Table } from 'antd'
3
+ import { CloseCircleOutlined, BarChartOutlined } from '@ant-design/icons'
4
+ import { formatBytes, formatDuration, formatRate, groupOf, selectPrimaryNetwork, sortDisks } from './monitor-model'
5
+ import { copy } from '../../common/clipboard'
6
+ import './monitor-details.styl'
7
+
8
+ const e = window.translate
9
+
10
+ // store.isMobile is the global 600px rule (mobileBreakpoint in ../../common/constants.js),
11
+ // used instead of antd's own `responsive` breakpoints so there is a single source of truth.
12
+ const desktopOnly = columns => window.store.isMobile ? [] : columns
13
+
14
+ export function Sparkline ({ history, level = 'normal', large = false }) {
15
+ if (!history.length) {
16
+ return <span className='remote-monitor-empty'>—</span>
17
+ }
18
+ const width = large ? 360 : 72
19
+ const height = large ? 88 : 20
20
+ const start = history[0].timestamp
21
+ const end = history[history.length - 1].timestamp
22
+ const span = Math.max(1, end - start)
23
+ const segments = []
24
+ let current = []
25
+ history.forEach((sample, index) => {
26
+ if (index && sample.timestamp - history[index - 1].timestamp > 12000) {
27
+ if (current.length) segments.push(current)
28
+ current = []
29
+ }
30
+ const x = (sample.timestamp - start) * width / span
31
+ const y = height - Math.max(0, Math.min(100, sample.value)) * height / 100
32
+ current.push(`${x.toFixed(1)},${y.toFixed(1)}`)
33
+ })
34
+ if (current.length) segments.push(current)
35
+ return (
36
+ <svg
37
+ aria-label={`${e('cpuHistory')}: ${Math.round(history[history.length - 1].value)}%`}
38
+ className={`remote-monitor-sparkline remote-monitor-level-${level}`}
39
+ height={height}
40
+ role='img'
41
+ viewBox={`0 0 ${width} ${height}`}
42
+ width={width}
43
+ >
44
+ {
45
+ segments.map((points, index) => (
46
+ <polyline
47
+ fill='none'
48
+ key={`${points[0]}-${index}`}
49
+ points={points.join(' ')}
50
+ stroke='currentColor'
51
+ strokeLinecap='round'
52
+ strokeLinejoin='round'
53
+ strokeWidth={large ? 2 : 1.5}
54
+ />
55
+ ))
56
+ }
57
+ </svg>
58
+ )
59
+ }
60
+
61
+ function DetailRows ({ rows }) {
62
+ return (
63
+ <dl className='remote-monitor-detail-rows'>
64
+ {
65
+ rows.filter(row => row[1] !== null && row[1] !== undefined && row[1] !== '').map(row => (
66
+ <div className='remote-monitor-detail-row' key={row[0]}>
67
+ <dt>{row[0]}</dt>
68
+ <dd title={String(row[1])}>{row[1]}</dd>
69
+ </div>
70
+ ))
71
+ }
72
+ </dl>
73
+ )
74
+ }
75
+
76
+ // antd Table only contains its own horizontal overflow when `scroll.x` is set: rc-table
77
+ // then wraps the content in an overflow-x:auto box and gives the inner table
78
+ // `width: x; min-width: 100%`. A *numeric* x (not 'max-content') keeps table-layout:
79
+ // fixed, so `ellipsis` columns still truncate and the width-less column soaks up the
80
+ // slack — 'max-content' would switch us back to auto layout and let a long process
81
+ // command stretch the table instead of ellipsising it.
82
+ // Without scroll.x the widest tables (network 460px, disks 452px) blow past the popover
83
+ // content box and drag the whole panel — heading and info button included — into a
84
+ // horizontal scrollbar. (`.remote-monitor-popover` is sized 488px so its 464px content
85
+ // box clears even the network table; scroll.x is the fallback for narrower widths.)
86
+ // Columns without a width are the flexible ones; give them a floor so x stays a
87
+ // realistic minimum instead of collapsing to just the fixed columns.
88
+ const FLEX_COLUMN_MIN_WIDTH = 100
89
+
90
+ const minTableWidth = columns => columns.reduce(
91
+ (sum, column) => sum + (typeof column.width === 'number' ? column.width : FLEX_COLUMN_MIN_WIDTH),
92
+ 0
93
+ )
94
+
95
+ // antd Table gives us column ellipsis and the compact size for free, so no
96
+ // hand rolled markup or table css is needed here.
97
+ function DetailTable ({ columns, rows, rowKey }) {
98
+ const mergedColumns = columns.map(withCellCopy)
99
+ return (
100
+ <Table
101
+ className='remote-monitor-table'
102
+ columns={mergedColumns}
103
+ dataSource={rows}
104
+ pagination={false}
105
+ rowKey={rowKey}
106
+ scroll={{ x: minTableWidth(mergedColumns) }}
107
+ size='small'
108
+ />
109
+ )
110
+ }
111
+
112
+ // Click a value cell to copy its text. Columns opt in with `copyValue(row)`, so the
113
+ // kill button and any other interactive cell stays untouched. `copy`() writes through
114
+ // window.pre.writeClipboard and fires the shared `copied` toast on its own.
115
+ // A finished text selection wins over the click, so drag selecting inside a cell
116
+ // never clobbers the clipboard by accident.
117
+ function withCellCopy (column) {
118
+ const { copyValue } = column
119
+ if (!copyValue) {
120
+ return column
121
+ }
122
+ const rest = { ...column }
123
+ delete rest.copyValue
124
+ return {
125
+ ...rest,
126
+ onCell: row => ({
127
+ className: 'remote-monitor-copy-cell',
128
+ onClick: () => {
129
+ if (window.getSelection()?.toString()) {
130
+ return
131
+ }
132
+ copy(String(copyValue(row)))
133
+ }
134
+ })
135
+ }
136
+ }
137
+
138
+ function GroupState ({ group }) {
139
+ if (group.data !== null && group.data !== undefined) {
140
+ if (group.status === 'stale') {
141
+ return (
142
+ <div className='remote-monitor-detail-state remote-monitor-level-warning'>
143
+ {e('stale')}: {new Date(group.updatedAt).toLocaleTimeString()}
144
+ </div>
145
+ )
146
+ }
147
+ return null
148
+ }
149
+ const text = group.status === 'loading' || group.status === 'idle'
150
+ ? e('loading')
151
+ : group.status === 'unsupported'
152
+ ? e('unavailable')
153
+ : group.error || e('unavailable')
154
+ return <div className='remote-monitor-detail-state'>{text}</div>
155
+ }
156
+
157
+ function ActivityDetails ({ group, sortBy, onKillProcess }) {
158
+ const activities = Array.isArray(group.data) ? [...group.data] : []
159
+ activities.sort(sortBy === 'memory'
160
+ ? (a, b) => b.memBytes - a.memBytes || b.cpu - a.cpu
161
+ : (a, b) => b.cpu - a.cpu || b.memBytes - a.memBytes)
162
+ const rows = sortBy ? activities.slice(0, 10) : activities
163
+ return (
164
+ <div className='remote-monitor-activity'>
165
+ {sortBy && <div className='remote-monitor-detail-heading'>{e('activity')}</div>}
166
+ <GroupState group={group} />
167
+ {
168
+ rows.length
169
+ ? (
170
+ <DetailTable
171
+ columns={[
172
+ ...(onKillProcess
173
+ ? [{
174
+ key: 'kill',
175
+ title: '',
176
+ width: 32,
177
+ align: 'center',
178
+ render: (_, row) => (
179
+ <Popconfirm title={`${e('close')} pid: ${row.pid}?`} onConfirm={() => onKillProcess(row.pid)}>
180
+ <Button size='small' type='text' icon={<CloseCircleOutlined />} aria-label={`${e('close')} ${row.pid}`} />
181
+ </Popconfirm>
182
+ )
183
+ }]
184
+ : []),
185
+ { key: 'pid', title: 'PID', dataIndex: 'pid', width: 60, copyValue: row => row.pid },
186
+ ...desktopOnly([{ key: 'user', title: e('users'), dataIndex: 'user', ellipsis: true, width: 64, copyValue: row => row.user }]),
187
+ { key: 'cpu', title: 'CPU', width: 52, render: (_, row) => `${row.cpu}%`, copyValue: row => `${row.cpu}%` },
188
+ { key: 'memory', title: e('memory'), width: 72, render: (_, row) => formatBytes(row.memBytes), copyValue: row => formatBytes(row.memBytes) },
189
+ { key: 'process', title: e('process'), dataIndex: 'cmd', ellipsis: true, copyValue: row => row.cmd }
190
+ ]}
191
+ rowKey={row => row.pid}
192
+ rows={rows}
193
+ />
194
+ )
195
+ : null
196
+ }
197
+ </div>
198
+ )
199
+ }
200
+
201
+ function CpuDetail ({ snapshot, includeActivity, level }) {
202
+ const group = groupOf(snapshot, 'cpu')
203
+ const values = snapshot.cpuHistory.map(sample => sample.value)
204
+ const average = values.length
205
+ ? values.reduce((sum, value) => sum + value, 0) / values.length
206
+ : null
207
+ return (
208
+ <>
209
+ <GroupState group={group} />
210
+ {
211
+ Number.isFinite(group.data)
212
+ ? (
213
+ <>
214
+ <DetailRows rows={[
215
+ [e('current'), `${group.data.toFixed(1)}%`],
216
+ [e('average'), average === null ? null : `${average.toFixed(1)}%`],
217
+ [e('minimum'), values.length ? `${Math.min(...values).toFixed(1)}%` : null],
218
+ [e('maximum'), values.length ? `${Math.max(...values).toFixed(1)}%` : null]
219
+ ]}
220
+ />
221
+ <div className='remote-monitor-detail-chart'>
222
+ <Sparkline history={snapshot.cpuHistory} large level={level} />
223
+ </div>
224
+ </>
225
+ )
226
+ : null
227
+ }
228
+ {includeActivity ? <ActivityDetails group={groupOf(snapshot, 'activities')} sortBy='cpu' /> : null}
229
+ </>
230
+ )
231
+ }
232
+
233
+ function MemoryDetail ({ snapshot, includeActivity, swapOnly }) {
234
+ const group = groupOf(snapshot, 'memory')
235
+ const memory = group.data
236
+ return (
237
+ <>
238
+ <GroupState group={group} />
239
+ {
240
+ memory
241
+ ? (
242
+ <DetailRows rows={[
243
+ [e('used'), swapOnly ? null : `${formatBytes(memory.usedBytes)} (${memory.percent.toFixed(1)}%)`],
244
+ [e('available'), swapOnly ? null : formatBytes(memory.availableBytes)],
245
+ [e('total'), swapOnly ? null : formatBytes(memory.totalBytes)],
246
+ [e('swap'), memory.swapTotalBytes === null ? null : `${formatBytes(memory.swapUsedBytes)} / ${formatBytes(memory.swapTotalBytes)}`],
247
+ [e('compatibilityMemory'), !swapOnly && memory.compatibilityMode ? e('enabled') : null]
248
+ ]}
249
+ />
250
+ )
251
+ : null
252
+ }
253
+ {includeActivity && <ActivityDetails group={groupOf(snapshot, 'activities')} sortBy='memory' />}
254
+ </>
255
+ )
256
+ }
257
+
258
+ function NetworkDetail ({ snapshot, direction, hideIP }) {
259
+ const group = groupOf(snapshot, 'network')
260
+ const network = group.data
261
+ const primary = selectPrimaryNetwork(network)
262
+ const directions = direction === 'network' ? ['upload', 'download'] : [direction]
263
+ const columns = [
264
+ {
265
+ key: 'name',
266
+ title: e('interface'),
267
+ width: 84,
268
+ ellipsis: true,
269
+ render: (_, row) => row.name === primary?.name ? `${row.name} *` : row.name
270
+ },
271
+ ...(hideIP
272
+ ? []
273
+ : [{
274
+ key: 'address',
275
+ title: e('address'),
276
+ width: 96,
277
+ ellipsis: true,
278
+ render: (_, row) => row.ipv4 || '—'
279
+ }]),
280
+ ...directions.flatMap(direction => {
281
+ const upload = direction === 'upload'
282
+ return [
283
+ {
284
+ key: direction,
285
+ title: e(direction),
286
+ width: 68,
287
+ render: (_, row) => formatRate(upload ? row.txRate : row.rxRate)
288
+ },
289
+ ...desktopOnly([
290
+ {
291
+ key: `${direction}-total`,
292
+ title: upload ? e('sent') : e('received'),
293
+ width: 72,
294
+ render: (_, row) => formatBytes(upload ? row.txBytes : row.rxBytes)
295
+ }
296
+ ])
297
+ ]
298
+ })
299
+ ]
300
+ return (
301
+ <>
302
+ <GroupState group={group} />
303
+ {
304
+ network
305
+ ? (
306
+ <>
307
+ <DetailRows rows={[
308
+ [e('primaryInterface'), primary?.name]
309
+ ]}
310
+ />
311
+ <DetailTable
312
+ columns={columns}
313
+ rowKey={row => row.name}
314
+ rows={network.interfaces || []}
315
+ />
316
+ </>
317
+ )
318
+ : null
319
+ }
320
+ </>
321
+ )
322
+ }
323
+
324
+ function UsersDetail ({ snapshot, hideIP }) {
325
+ const group = groupOf(snapshot, 'users')
326
+ const users = group.data
327
+ return (
328
+ <>
329
+ <GroupState group={group} />
330
+ {
331
+ users
332
+ ? users.sessions.length
333
+ ? (
334
+ <DetailTable
335
+ columns={[
336
+ { key: 'user', title: e('users'), dataIndex: 'user', ellipsis: true, width: 80 },
337
+ ...desktopOnly([{ key: 'terminal', title: 'TTY', width: 64, ellipsis: true, render: (_, row) => row.terminal || '—' }]),
338
+ { key: 'time', title: e('sessions'), width: 116, ellipsis: true, render: (_, row) => row.loginTime || '—' },
339
+ ...(hideIP
340
+ ? []
341
+ : desktopOnly([{ key: 'source', title: e('address'), width: 96, ellipsis: true, render: (_, row) => row.source || '—' }]))
342
+ ]}
343
+ rows={users.sessions}
344
+ />
345
+ )
346
+ : <div className='remote-monitor-detail-state'>{e('noUsers')}</div>
347
+ : null
348
+ }
349
+ </>
350
+ )
351
+ }
352
+
353
+ function DisksDetail ({ snapshot, levels }) {
354
+ const group = groupOf(snapshot, 'disks')
355
+ const disks = sortDisks(group.data)
356
+ return (
357
+ <>
358
+ <GroupState group={group} />
359
+ {
360
+ disks.length
361
+ ? (
362
+ <DetailTable
363
+ columns={[
364
+ { key: 'mount', title: e('mount'), dataIndex: 'mount', ellipsis: true, width: 72 },
365
+ { key: 'usage', title: e('used'), width: 60, render: (_, row) => <span className={`remote-monitor-level-${levels[row.mount] || 'unknown'}`}>{row.percent}%</span> },
366
+ { key: 'used', title: e('used'), width: 72, ellipsis: true, render: (_, row) => formatBytes(row.usedBytes) },
367
+ ...desktopOnly([{ key: 'available', title: e('available'), width: 76, ellipsis: true, render: (_, row) => formatBytes(row.availableBytes) }]),
368
+ { key: 'total', title: e('total'), width: 72, ellipsis: true, render: (_, row) => formatBytes(row.totalBytes) },
369
+ ...desktopOnly([{ key: 'filesystem', title: e('filesystem'), dataIndex: 'filesystem', ellipsis: true, width: 100 }])
370
+ ]}
371
+ rowKey={row => `${row.filesystem}-${row.mount}`}
372
+ rows={disks}
373
+ />
374
+ )
375
+ : null
376
+ }
377
+ </>
378
+ )
379
+ }
380
+
381
+ // auto so a resize that flips store.isMobile re-renders the columns right away
382
+ export default auto(function MonitorDetails ({ id, snapshot, tab, config, levels, onClose, includeActivity = true, onKillProcess }) {
383
+ const sysInfo = groupOf(snapshot, 'sysinfo')
384
+ let body
385
+ if (id === 'hostname') {
386
+ body = (
387
+ <>
388
+ <GroupState group={sysInfo} />
389
+ <DetailRows rows={[
390
+ [e('hostname'), sysInfo.data?.hostname],
391
+ [e('address'), config.hideIP ? null : tab.host],
392
+ ['OS', sysInfo.data?.os],
393
+ ['Kernel', sysInfo.data?.kernel],
394
+ ['Arch', sysInfo.data?.arch],
395
+ ['Shell', [sysInfo.data?.shell, sysInfo.data?.shellVersion].filter(Boolean).join(' ')]
396
+ ]}
397
+ />
398
+ </>
399
+ )
400
+ } else if (id === 'cpu') {
401
+ body = <CpuDetail includeActivity={includeActivity} level={levels.cpu} snapshot={snapshot} />
402
+ } else if (id === 'cpuHistory') {
403
+ body = <CpuDetail level={levels.cpu} snapshot={snapshot} />
404
+ } else if (id === 'memory' || id === 'swap') {
405
+ body = <MemoryDetail snapshot={snapshot} includeActivity={includeActivity} swapOnly={id === 'swap'} />
406
+ } else if (id === 'upload' || id === 'download' || id === 'network') {
407
+ body = <NetworkDetail direction={id} hideIP={config.hideIP} snapshot={snapshot} />
408
+ } else if (id === 'uptime') {
409
+ const group = groupOf(snapshot, 'uptime')
410
+ body = (
411
+ <>
412
+ <GroupState group={group} />
413
+ {group.data
414
+ ? <DetailRows rows={[
415
+ [e('uptime'), formatDuration(group.data.seconds)],
416
+ [e('bootTime'), group.data.bootTime ? new Date(group.data.bootTime).toLocaleString() : null]
417
+ ]}
418
+ />
419
+ : null}
420
+ </>
421
+ )
422
+ } else if (id === 'users') {
423
+ body = <UsersDetail snapshot={snapshot} hideIP={config.hideIP} />
424
+ } else if (id === 'activities') {
425
+ body = <ActivityDetails group={groupOf(snapshot, 'activities')} onKillProcess={onKillProcess} />
426
+ } else if (id === 'disks') {
427
+ body = <DisksDetail levels={levels.disks} snapshot={snapshot} />
428
+ }
429
+ return (
430
+ <div
431
+ className={'remote-monitor-details' + (onClose ? ' remote-monitor-popover' : '')}
432
+ data-monitor-detail={id}
433
+ onKeyDown={event => {
434
+ if (onClose && event.key === 'Escape') {
435
+ event.stopPropagation()
436
+ onClose()
437
+ }
438
+ }}
439
+ >
440
+ <div className='remote-monitor-detail-heading'>{e(id)}</div>
441
+ {body}
442
+ {onClose && (
443
+ <Button
444
+ icon={<BarChartOutlined />}
445
+ onClick={() => {
446
+ onClose()
447
+ window.store.openInfoPanel()
448
+ }}
449
+ onMouseDown={event => event.preventDefault()}
450
+ size='small'
451
+ type='link'
452
+ >
453
+ {e('info')}
454
+ </Button>
455
+ )}
456
+ </div>
457
+ )
458
+ })
@@ -0,0 +1,87 @@
1
+ .remote-monitor-sparkline
2
+ display block
3
+ overflow visible
4
+ color var(--primary)
5
+ transition color 120ms ease
6
+
7
+ .remote-monitor-empty
8
+ color var(--text-disabled)
9
+
10
+ .remote-monitor-popover
11
+ // 488 - antd popover innerPadding 12*2 = 464 content, just clears the widest table
12
+ // (network, Σ列宽 460) so the desktop popover never needs a horizontal scrollbar
13
+ width 488px
14
+ max-width calc(100vw - 32px)
15
+ max-height calc(100dvh - 80px - var(--shortcut-bar-kb-offset, 0px))
16
+ // tables own their own horizontal overflow via scroll.x, so the panel never needs
17
+ // to scroll sideways (that would drag the heading and the info button along)
18
+ overflow-x hidden
19
+ overflow-y auto
20
+
21
+ .remote-monitor-detail-heading
22
+ margin-bottom 8px
23
+ color var(--text)
24
+ font-size 13px
25
+ font-weight 600
26
+
27
+ .remote-monitor-detail-state
28
+ margin 6px 0
29
+ color var(--text-disabled)
30
+
31
+ .remote-monitor-detail-rows
32
+ margin 0 0 8px
33
+
34
+ .remote-monitor-detail-row
35
+ display grid
36
+ grid-template-columns minmax(0, 0.45fr) minmax(0, 1fr)
37
+ gap 10px
38
+ padding 3px 0
39
+ dt
40
+ color var(--text-dark)
41
+ dd
42
+ min-width 0
43
+ margin 0
44
+ overflow hidden
45
+ text-overflow ellipsis
46
+ color var(--text)
47
+ font-variant-numeric tabular-nums
48
+
49
+ .remote-monitor-detail-chart
50
+ display flex
51
+ align-items center
52
+ justify-content center
53
+ margin 8px 0
54
+ padding 4px
55
+ .remote-monitor-sparkline
56
+ max-width 100%
57
+
58
+ .remote-monitor-activity
59
+ margin-top 10px
60
+
61
+ // antd Table owns the whole table layout, only the dense metrics look is kept
62
+ .remote-monitor-table
63
+ font-variant-numeric tabular-nums
64
+ .ant-table.ant-table-small
65
+ font-size 11px
66
+
67
+ // click a value cell copies it (see withCellCopy)
68
+ .remote-monitor-copy-cell
69
+ cursor pointer
70
+ &:hover
71
+ color var(--primary)
72
+
73
+ .remote-monitor-item.remote-monitor-level-warning,
74
+ .remote-monitor-level-warning
75
+ color var(--warn)
76
+
77
+ .remote-monitor-item.remote-monitor-level-critical,
78
+ .remote-monitor-level-critical
79
+ color var(--error)
80
+
81
+ .remote-monitor-item.remote-monitor-level-unknown,
82
+ .remote-monitor-level-unknown
83
+ color var(--text-disabled)
84
+
85
+ @media (prefers-reduced-motion: reduce)
86
+ .remote-monitor-sparkline
87
+ transition none