@rapidmx/web-client 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/apps/shared/components/admin/diagnostics/HostCard.tsx +2 -0
- package/apps/shared/components/admin/diagnostics/PressureTiles.tsx +108 -0
- package/apps/shared/components/admin/diagnostics/PvcTable.tsx +20 -6
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +21 -2
- package/apps/shared/components/admin/layout/AdminShell.tsx +2 -2
- package/apps/shared/components/admin/settings/PluginsManager.tsx +508 -35
- package/apps/shared/components/admin/settings/pluginPreferences.ts +34 -0
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +2 -2
- package/apps/shared/components/layout/AppShell.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +18 -9
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +24 -0
- package/apps/shared/components/mail/listPreferences.ts +3 -3
- package/apps/www/index.tsx +62 -12
- package/dist/apps/shared/components/admin/diagnostics/HostCard.js +2 -1
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.d.ts +22 -0
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.js +52 -0
- package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +9 -4
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +35 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +267 -35
- package/dist/apps/shared/components/admin/settings/pluginPreferences.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/pluginPreferences.js +35 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +2 -2
- package/dist/apps/shared/components/layout/AppShell.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.d.ts +10 -3
- package/dist/apps/shared/components/mail/ConversationList.js +9 -5
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +20 -1
- package/dist/apps/shared/components/mail/listPreferences.d.ts +1 -1
- package/dist/apps/www/index.js +51 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -357,7 +357,7 @@ Plugin pages import these directly from `@rapidmx/react-shared`, which the serve
|
|
|
357
357
|
use:
|
|
358
358
|
|
|
359
359
|
- `branding/useBranding.js`: `useBranding()`, for the branding and icon of pages outside a shell;
|
|
360
|
-
- `auth/session.js`: `
|
|
360
|
+
- `auth/session.js`: `useSessionRefresh()`, already called by every shell, which renews the sign-in before its hour is up;
|
|
361
361
|
- `util/api.js`: `apiFetch()` and `ApiRequestError`, for calling the plugin's own API routes;
|
|
362
362
|
- `mail/mailApi.js`: mailboxes and folders, such as `listMailboxes()` and `listFolders()`;
|
|
363
363
|
- `components/buttons/Button.js`, `components/feedback/Alert.js` and `components/feedback/Skeleton.js`;
|
|
@@ -7,6 +7,7 @@ import type { DiagnosticsDiskMetrics, DiagnosticsMetrics } from "./diagnosticsAp
|
|
|
7
7
|
import { formatBytes, formatCores, formatPercent, NO_VALUE, percentOf } from "./format.js";
|
|
8
8
|
import { seriesOf } from "./metricsHistory.js";
|
|
9
9
|
import MetricTile from "./MetricTile.js";
|
|
10
|
+
import PressureTiles from "./PressureTiles.js";
|
|
10
11
|
import UsageMeter from "./UsageMeter.js";
|
|
11
12
|
|
|
12
13
|
const percentLabel = (value: number) => formatPercent(value);
|
|
@@ -91,6 +92,7 @@ export default function HostCard({ host, history }: HostCardProps) {
|
|
|
91
92
|
/>
|
|
92
93
|
<MetricTile label="Load average" value={formatLoad(host.loadAverage)} detail="1, 5 and 15 minutes" />
|
|
93
94
|
</div>
|
|
95
|
+
<PressureTiles host={host} history={history} />
|
|
94
96
|
<h3 className="mt-4 mb-2 text-xs uppercase tracking-wide text-text-muted">Disks</h3>
|
|
95
97
|
{disks.length === 0 ? (
|
|
96
98
|
<p className="text-sm text-text-muted">No disks were reported.</p>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { DiagnosticsMetrics, DiagnosticsPressure } from "./diagnosticsApi.js";
|
|
7
|
+
import { formatBytes, formatPercent, percentOf } from "./format.js";
|
|
8
|
+
import { seriesOf } from "./metricsHistory.js";
|
|
9
|
+
import MetricTile from "./MetricTile.js";
|
|
10
|
+
|
|
11
|
+
/** From this share of the last 10 seconds spent stalled, pressure is called elevated ... */
|
|
12
|
+
export const ELEVATED_PRESSURE_PERCENT = 10;
|
|
13
|
+
|
|
14
|
+
/** ... and from this, high. Kernel documentation treats a few percent as ordinary and tens of percent as a machine that is struggling. */
|
|
15
|
+
export const HIGH_PRESSURE_PERCENT = 30;
|
|
16
|
+
|
|
17
|
+
export type PressureLevel = "Low" | "Elevated" | "High";
|
|
18
|
+
|
|
19
|
+
/** Pressure in words, so that it is never only a number or a colour. */
|
|
20
|
+
export function pressureLevel(percent: number): PressureLevel {
|
|
21
|
+
if (percent >= HIGH_PRESSURE_PERCENT) {
|
|
22
|
+
return "High";
|
|
23
|
+
}
|
|
24
|
+
return percent >= ELEVATED_PRESSURE_PERCENT ? "Elevated" : "Low";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Host = NonNullable<DiagnosticsMetrics["host"]>;
|
|
28
|
+
type Resource = keyof NonNullable<Host["pressure"]>;
|
|
29
|
+
|
|
30
|
+
const RESOURCES: { key: Resource; label: string; waiting: string }[] = [
|
|
31
|
+
{ key: "memory", label: "Memory pressure", waiting: "waiting for memory" },
|
|
32
|
+
{ key: "io", label: "I/O pressure", waiting: "waiting for the disk" },
|
|
33
|
+
{ key: "cpu", label: "CPU pressure", waiting: "waiting for a CPU" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function PressureTile({ label, waiting, stall, history, resource }: { label: string; waiting: string; stall: DiagnosticsPressure; history: DiagnosticsMetrics[]; resource: Resource }) {
|
|
37
|
+
const level = pressureLevel(stall.some.avg10);
|
|
38
|
+
return (
|
|
39
|
+
<MetricTile
|
|
40
|
+
label={label}
|
|
41
|
+
value={formatPercent(stall.some.avg10)}
|
|
42
|
+
detail={`${level}: tasks ${waiting}, last 10 seconds`}
|
|
43
|
+
history={{
|
|
44
|
+
values: seriesOf(history, (sample) => sample.host?.pressure?.[resource]?.some.avg10),
|
|
45
|
+
max: 100,
|
|
46
|
+
format: formatPercent,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<div className="mt-2 text-xs text-text-muted">
|
|
50
|
+
Last minute {formatPercent(stall.some.avg60)}, last 5 minutes {formatPercent(stall.some.avg300)}
|
|
51
|
+
{stall.full && ` · every task waiting: ${formatPercent(stall.full.avg10)}`}
|
|
52
|
+
</div>
|
|
53
|
+
</MetricTile>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PressureTilesProps {
|
|
58
|
+
host: Host;
|
|
59
|
+
history: DiagnosticsMetrics[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How stalled the node is (Linux pressure stall information: the share of recent time tasks spent waiting for memory, the disk or
|
|
64
|
+
* a CPU), and how much memory the hypervisor holds in the machine's balloon. Both explain a node that is slow while nothing on it
|
|
65
|
+
* uses much: a guest counts ballooned memory as used though no process holds it, and a machine short of memory stalls on the disk.
|
|
66
|
+
* Nothing is shown where the kernel gives no such figures.
|
|
67
|
+
*/
|
|
68
|
+
export default function PressureTiles({ host, history }: PressureTilesProps) {
|
|
69
|
+
const tiles = RESOURCES.filter(({ key }) => host.pressure?.[key]);
|
|
70
|
+
const balloon = host.balloon;
|
|
71
|
+
if (tiles.length === 0 && !balloon) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const heldPercent = percentOf(balloon?.heldBytes, host.memoryTotalBytes);
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<h3 className="mt-4 mb-2 text-xs uppercase tracking-wide text-text-muted">Pressure</h3>
|
|
78
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
|
|
79
|
+
{RESOURCES.map(({ key, label, waiting }) => {
|
|
80
|
+
const stall = host.pressure?.[key];
|
|
81
|
+
return stall && <PressureTile key={key} label={label} waiting={waiting} stall={stall} history={history} resource={key} />;
|
|
82
|
+
})}
|
|
83
|
+
{balloon && (
|
|
84
|
+
<MetricTile
|
|
85
|
+
label="Held by the hypervisor"
|
|
86
|
+
value={formatBytes(balloon.heldBytes)}
|
|
87
|
+
detail={`of ${formatBytes(host.memoryTotalBytes)}`}
|
|
88
|
+
usage={
|
|
89
|
+
heldPercent === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: { percent: heldPercent, detail: `${formatBytes(balloon.heldBytes)} in the memory balloon`, flag: false }
|
|
92
|
+
}
|
|
93
|
+
history={{
|
|
94
|
+
values: seriesOf(history, (sample) => sample.host?.balloon?.heldBytes),
|
|
95
|
+
max: host.memoryTotalBytes,
|
|
96
|
+
format: formatBytes,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<div className="mt-2 text-xs text-text-muted">
|
|
100
|
+
Memory the hypervisor has taken back through the balloon. It counts as used above though no process holds it.{" "}
|
|
101
|
+
{formatBytes(balloon.inflatedTotalBytes)} taken since the node booted.
|
|
102
|
+
</div>
|
|
103
|
+
</MetricTile>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -7,26 +7,40 @@ import type { DiagnosticsPvcMetrics } from "./diagnosticsApi.js";
|
|
|
7
7
|
import { formatBytes, NO_VALUE, percentOf } from "./format.js";
|
|
8
8
|
import UsageMeter from "./UsageMeter.js";
|
|
9
9
|
|
|
10
|
-
const HEADINGS = ["Volume", "Phase", "Storage class", "
|
|
10
|
+
const HEADINGS = ["Volume", "Phase", "Storage class", "Allocated", "Usage"];
|
|
11
11
|
|
|
12
12
|
function Usage({ pvc }: { pvc: DiagnosticsPvcMetrics }) {
|
|
13
13
|
if (!pvc.mountedByServer) {
|
|
14
14
|
return <span className="text-text-muted">Usage not available: not mounted in the server pod.</span>;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
// On the node's disk the filesystem's figures are the disk's, so only a measured directory says what the volume holds (an
|
|
17
|
+
// older server sent the disk's figures without saying so).
|
|
18
|
+
const own = pvc.sharesNodeDisk && pvc.measuredBy !== "directory" ? undefined : pvc.usedBytes;
|
|
19
|
+
const percent = percentOf(own, pvc.capacityBytes);
|
|
17
20
|
if (percent === undefined) {
|
|
18
|
-
return
|
|
21
|
+
return (
|
|
22
|
+
<span className="text-text-muted">
|
|
23
|
+
{pvc.sharesNodeDisk
|
|
24
|
+
? "Shares the node\u2019s disk, and what the volume holds could not be measured."
|
|
25
|
+
: "The server has no usage figures for this volume."}
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
19
28
|
}
|
|
20
29
|
return (
|
|
21
30
|
<div>
|
|
22
31
|
<UsageMeter
|
|
23
32
|
label={`Usage of volume ${pvc.name}`}
|
|
24
33
|
percent={percent}
|
|
25
|
-
detail={`${formatBytes(
|
|
26
|
-
flag={!pvc.sharesNodeDisk}
|
|
34
|
+
detail={`${pvc.usedPartial ? "At least " : ""}${formatBytes(own)} of ${formatBytes(pvc.capacityBytes)} allocated`}
|
|
27
35
|
/>
|
|
36
|
+
{pvc.usedPartial && (
|
|
37
|
+
<p className="mt-1 text-xs text-text-muted">The volume is too large to measure completely, so this is a lower bound.</p>
|
|
38
|
+
)}
|
|
28
39
|
{pvc.sharesNodeDisk && (
|
|
29
|
-
<p className="mt-1 text-xs text-text-muted">
|
|
40
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
41
|
+
Measured from the volume’s directory. It shares the node’s disk, which has {formatBytes(pvc.availableBytes)} free; the
|
|
42
|
+
volume’s allocation is not enforced.
|
|
43
|
+
</p>
|
|
30
44
|
)}
|
|
31
45
|
</div>
|
|
32
46
|
);
|
|
@@ -124,9 +124,22 @@ export interface DiagnosticsHostMetrics {
|
|
|
124
124
|
memoryTotalBytes: number;
|
|
125
125
|
memoryUsedBytes: number;
|
|
126
126
|
loadAverage: [number, number, number];
|
|
127
|
+
/**
|
|
128
|
+
* Memory the hypervisor holds in the machine's memory balloon. It counts in `memoryUsedBytes` (a guest sees ballooned memory
|
|
129
|
+
* as used) though no process holds it. Absent where no balloon has ever taken any.
|
|
130
|
+
*/
|
|
131
|
+
balloon?: { heldBytes: number; inflatedTotalBytes: number };
|
|
132
|
+
/** How stalled the node is waiting for memory, disk and CPU (Linux pressure stall information). Absent without kernel support. */
|
|
133
|
+
pressure?: { memory?: DiagnosticsPressure; io?: DiagnosticsPressure; cpu?: DiagnosticsPressure };
|
|
127
134
|
disks: DiagnosticsDiskMetrics[];
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
/** The share (0 to 100) of the last 10, 60 and 300 seconds that tasks were stalled. `some`: at least one waited. `full`: every one did. */
|
|
138
|
+
export interface DiagnosticsPressure {
|
|
139
|
+
some: { avg10: number; avg60: number; avg300: number };
|
|
140
|
+
full?: { avg10: number; avg60: number; avg300: number };
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
/** From the metrics API (metrics-server): absent when it is not installed. */
|
|
131
144
|
export interface DiagnosticsPodMetrics {
|
|
132
145
|
name: string;
|
|
@@ -136,8 +149,11 @@ export interface DiagnosticsPodMetrics {
|
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
/**
|
|
139
|
-
* `usedBytes
|
|
140
|
-
*
|
|
152
|
+
* `usedBytes` (what the volume holds) exists only for a volume the server pod mounts (`mountedByServer`), and `measuredBy`
|
|
153
|
+
* says how it was found: from the volume's own filesystem, or - when the volume is a directory of the node's disk
|
|
154
|
+
* (`sharesNodeDisk`, such as k3s's local-path), where the filesystem is the whole disk - by measuring the directory.
|
|
155
|
+
* `capacityBytes` is the volume's allocation, which a volume that shares the node's disk does not enforce; `availableBytes` is
|
|
156
|
+
* what is left of the filesystem it is on.
|
|
141
157
|
*/
|
|
142
158
|
export interface DiagnosticsPvcMetrics {
|
|
143
159
|
name: string;
|
|
@@ -147,6 +163,9 @@ export interface DiagnosticsPvcMetrics {
|
|
|
147
163
|
capacityBytes?: number;
|
|
148
164
|
usedBytes?: number;
|
|
149
165
|
availableBytes?: number;
|
|
166
|
+
measuredBy?: "filesystem" | "directory";
|
|
167
|
+
/** The directory could not be measured completely, so `usedBytes` is a lower bound. */
|
|
168
|
+
usedPartial?: boolean;
|
|
150
169
|
mountedByServer: boolean;
|
|
151
170
|
sharesNodeDisk?: boolean;
|
|
152
171
|
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "react-icons/hi2";
|
|
28
28
|
import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
29
29
|
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
30
|
-
import {
|
|
30
|
+
import { useSessionRefresh } from "@rapidmx/react-shared/auth/session.js";
|
|
31
31
|
import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
|
|
32
32
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
33
33
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
@@ -197,7 +197,7 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
197
197
|
const { branding, iconSrc } = useBranding();
|
|
198
198
|
const footer = useBrandingHtml(branding?.footerHtml);
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
useSessionRefresh(userUid, authServerUrl);
|
|
201
201
|
|
|
202
202
|
useEffect(() => {
|
|
203
203
|
if (!userUid) {
|