@scalio-cloud/ui 0.20.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/CHANGELOG.md +9 -0
- package/nav.css +40 -0
- package/package.json +1 -1
- package/react/index.js +1 -1
- package/react/selftest.js +277 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
Testmodus (scalio-docs ADR 0053, package scalio-selftest).
|
|
5
|
+
- `SelfTestPanel` for the page «Tests»: last runs with verdict, scope, source, version and counts; details per
|
|
6
|
+
section; «Neu fehlgeschlagen» and «Wieder in Ordnung» since the run before; «Jetzt ausführen» and «Abbrechen» with
|
|
7
|
+
live progress; schedule (scope and hour of the nightly run). `selftestApi(api)` maps the routes `/e2e/*`.
|
|
8
|
+
- `SelfTestBanner`: hint «Testmodus (SAFE)» while the test mode is on.
|
|
9
|
+
- CSS in `nav.css` (`.scalio-selftest*`).
|
|
10
|
+
- **Visible change:** only in apps that show the new page. No token changes.
|
|
11
|
+
|
|
3
12
|
## 0.20.0
|
|
4
13
|
Scalio Ideen (ADR 0050): feature requests from the apps, votes and comments, kept centrally in Compass.
|
|
5
14
|
- `supportApi(api, { ideas: true })` adds the routes of scalio-connect 0.8.0 (`ideas_router`). The support drawer then
|
package/nav.css
CHANGED
|
@@ -479,3 +479,43 @@
|
|
|
479
479
|
.dark .scalio-license-addon.tone-warn { border-color: rgb(251 191 36 / 0.4); background: rgb(251 191 36 / 0.08); }
|
|
480
480
|
.scalio-license-addon-error { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
481
481
|
.scalio-license-link { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
|
|
482
|
+
|
|
483
|
+
/* Self-check page (SelfTestPanel, ADR 0053) */
|
|
484
|
+
.scalio-selftest { border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--card)); color: hsl(var(--card-foreground)); font-size: 0.875rem; }
|
|
485
|
+
.scalio-selftest-part { padding: 1.25rem; }
|
|
486
|
+
.scalio-selftest-part + .scalio-selftest-part { border-top: 1px solid hsl(var(--border)); }
|
|
487
|
+
.scalio-selftest-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
488
|
+
.scalio-selftest-title { display: flex; align-items: center; gap: 0.5rem; font-family: 'Chivo', sans-serif; font-weight: 700; }
|
|
489
|
+
.scalio-selftest-title .scalio-icon { color: hsl(var(--primary)); }
|
|
490
|
+
.scalio-selftest-hint { margin: 0.25rem 0 0; color: hsl(var(--muted-foreground)); }
|
|
491
|
+
.scalio-selftest-actions { display: flex; gap: 0.5rem; }
|
|
492
|
+
.scalio-selftest-sub { display: flex; align-items: center; gap: 0.375rem; margin-bottom: 0.75rem; font-weight: 600; }
|
|
493
|
+
.scalio-selftest-live { margin-top: 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
494
|
+
.scalio-selftest-live-head { display: flex; justify-content: space-between; color: hsl(var(--muted-foreground)); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; }
|
|
495
|
+
.scalio-selftest-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.75rem; }
|
|
496
|
+
.scalio-selftest-form label { display: flex; flex-direction: column; gap: 0.25rem; min-width: 10rem; }
|
|
497
|
+
.scalio-selftest-form label span { color: hsl(var(--muted-foreground)); font-size: 0.75rem; }
|
|
498
|
+
.scalio-selftest-run { border-top: 1px solid hsl(var(--border)); padding: 0.625rem 0; }
|
|
499
|
+
.scalio-selftest-run:first-of-type { border-top: 0; }
|
|
500
|
+
.scalio-selftest-run-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
|
|
501
|
+
.scalio-selftest-when, .scalio-selftest-version { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; }
|
|
502
|
+
.scalio-selftest-source, .scalio-selftest-version { color: hsl(var(--muted-foreground)); }
|
|
503
|
+
.scalio-selftest-counts { display: inline-flex; gap: 0.5rem; font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem; }
|
|
504
|
+
.scalio-selftest-run-head .scalio-btn { margin-left: auto; }
|
|
505
|
+
.scalio-selftest-run-body { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 0.75rem; }
|
|
506
|
+
.scalio-selftest-run-body ul { margin: 0.25rem 0 0; padding-left: 1rem; }
|
|
507
|
+
.scalio-selftest-sections { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
508
|
+
.scalio-selftest-section-title { font-weight: 600; margin-bottom: 0.25rem; }
|
|
509
|
+
.scalio-selftest-section ul { list-style: none; margin: 0; padding: 0; }
|
|
510
|
+
.scalio-selftest-section li { display: grid; grid-template-columns: 3rem minmax(0, 1fr); gap: 0 0.5rem; padding: 0.1875rem 0; }
|
|
511
|
+
.scalio-selftest-status { font-family: 'JetBrains Mono', monospace; font-size: 0.6875rem; font-weight: 600; }
|
|
512
|
+
.scalio-selftest-detail { grid-column: 2; color: hsl(var(--muted-foreground)); font-size: 0.75rem; overflow-wrap: anywhere; }
|
|
513
|
+
.scalio-selftest .is-ok, .scalio-selftest li.is-ok .scalio-selftest-status { color: #15803d; }
|
|
514
|
+
.scalio-selftest .is-warn, .scalio-selftest li.is-warn .scalio-selftest-status { color: #b45309; }
|
|
515
|
+
.scalio-selftest .is-error, .scalio-selftest li.is-error .scalio-selftest-status { color: hsl(var(--destructive)); }
|
|
516
|
+
.scalio-selftest .is-neutral { color: hsl(var(--muted-foreground)); }
|
|
517
|
+
.dark .scalio-selftest .is-ok, .dark .scalio-selftest li.is-ok .scalio-selftest-status { color: #4ade80; }
|
|
518
|
+
.dark .scalio-selftest .is-warn, .dark .scalio-selftest li.is-warn .scalio-selftest-status { color: #fbbf24; }
|
|
519
|
+
.scalio-selftest .scalio-notice.is-ok, .scalio-selftest .scalio-notice.is-error { color: inherit; }
|
|
520
|
+
.scalio-selftest-banner { display: flex; align-items: center; gap: 0.5rem; }
|
|
521
|
+
@media (max-width: 640px) { .scalio-selftest-run-head .scalio-btn { margin-left: 0; } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
|
|
5
5
|
"license": "SEE LICENSE IN README.md",
|
|
6
6
|
"private": false,
|
package/react/index.js
CHANGED
|
@@ -378,6 +378,6 @@ module.exports = {
|
|
|
378
378
|
TEST_IDS,
|
|
379
379
|
initials,
|
|
380
380
|
};
|
|
381
|
-
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
|
|
381
|
+
Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"), require("./selftest"));
|
|
382
382
|
// After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
|
|
383
383
|
Object.assign(module.exports, require("./auth"));
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Page «Tests» of the test mode (scalio-docs ADR 0053, package scalio-selftest): last runs, live progress,
|
|
3
|
+
// run now / cancel, nightly schedule, results per section, what broke and what recovered since the run before.
|
|
4
|
+
// selftestApi(api) maps the routes of scalio-selftest (/e2e/*). SelfTestBanner shows the hint on every page.
|
|
5
|
+
const React = require("react");
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
const Icons = require("./icons");
|
|
8
|
+
|
|
9
|
+
const LABELS = {
|
|
10
|
+
de: {
|
|
11
|
+
title: "Selbsttest",
|
|
12
|
+
hint: "Diese Instanz prüft sich selbst von aussen: Schnittstellen und Browser, mit eigenen Testkonten.",
|
|
13
|
+
off: "Der Testmodus ist auf dieser Instanz aus (TEST_MODE und SCALIO_STAGE dev oder test). Gezeigt werden nur frühere Läufe.",
|
|
14
|
+
banner: "Testmodus ({suite}): Diese Instanz prüft sich automatisch selbst.",
|
|
15
|
+
runNow: "Jetzt ausführen",
|
|
16
|
+
queued: "Lauf angefordert. Er startet innerhalb einer Minute.",
|
|
17
|
+
cancel: "Abbrechen",
|
|
18
|
+
cancelling: "Abbruch angefordert.",
|
|
19
|
+
running: "Läuft",
|
|
20
|
+
waiting: "Wartet auf den Start",
|
|
21
|
+
checks: "Prüfungen",
|
|
22
|
+
never: "Noch keine Läufe.",
|
|
23
|
+
schedule: "Zeitplan",
|
|
24
|
+
suite: "Umfang",
|
|
25
|
+
hour: "Nachtlauf um",
|
|
26
|
+
noNight: "kein Nachtlauf",
|
|
27
|
+
save: "Speichern",
|
|
28
|
+
saved: "Zeitplan gespeichert.",
|
|
29
|
+
safeHint: "SAFE prüft nur mit eigenen Daten und simulierten Microsoft-Schichten. FULL schreibt auch in den Test-Tenant.",
|
|
30
|
+
runs: "Läufe",
|
|
31
|
+
newFailures: "Neu fehlgeschlagen",
|
|
32
|
+
recovered: "Wieder in Ordnung",
|
|
33
|
+
was: "vorher {s}",
|
|
34
|
+
sources: { manual: "von Hand", scheduled: "Nachtlauf", deploy: "nach Deploy" },
|
|
35
|
+
verdicts: { PASS: "Bestanden", WARN: "Warnung", FAIL: "Fehler", SKIP: "Übersprungen", CANCELLED: "Abgebrochen" },
|
|
36
|
+
show: "Details",
|
|
37
|
+
hide: "Ausblenden",
|
|
38
|
+
version: "Version",
|
|
39
|
+
},
|
|
40
|
+
en: {
|
|
41
|
+
title: "Self-check",
|
|
42
|
+
hint: "This instance checks itself from the outside: API and browser, with its own test accounts.",
|
|
43
|
+
off: "The test mode is off on this instance (TEST_MODE and SCALIO_STAGE dev or test). Only earlier runs are shown.",
|
|
44
|
+
banner: "Test mode ({suite}): this instance checks itself automatically.",
|
|
45
|
+
runNow: "Run now",
|
|
46
|
+
queued: "Run requested. It starts within a minute.",
|
|
47
|
+
cancel: "Cancel",
|
|
48
|
+
cancelling: "Cancel requested.",
|
|
49
|
+
running: "Running",
|
|
50
|
+
waiting: "Waiting to start",
|
|
51
|
+
checks: "checks",
|
|
52
|
+
never: "No runs yet.",
|
|
53
|
+
schedule: "Schedule",
|
|
54
|
+
suite: "Scope",
|
|
55
|
+
hour: "Nightly run at",
|
|
56
|
+
noNight: "no nightly run",
|
|
57
|
+
save: "Save",
|
|
58
|
+
saved: "Schedule saved.",
|
|
59
|
+
safeHint: "SAFE checks with own data and simulated Microsoft layers only. FULL also writes to the test tenant.",
|
|
60
|
+
runs: "Runs",
|
|
61
|
+
newFailures: "Newly failing",
|
|
62
|
+
recovered: "Recovered",
|
|
63
|
+
was: "was {s}",
|
|
64
|
+
sources: { manual: "by hand", scheduled: "nightly", deploy: "after deploy" },
|
|
65
|
+
verdicts: { PASS: "Passed", WARN: "Warning", FAIL: "Failed", SKIP: "Skipped", CANCELLED: "Cancelled" },
|
|
66
|
+
show: "Details",
|
|
67
|
+
hide: "Hide",
|
|
68
|
+
version: "Version",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function selftestLabels(lang, overrides) {
|
|
73
|
+
const base = LABELS[lang === "en" ? "en" : "de"];
|
|
74
|
+
return Object.assign({}, base, overrides || {});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TONE = { PASS: "ok", WARN: "warn", FAIL: "error", SKIP: "neutral", CANCELLED: "neutral" };
|
|
78
|
+
|
|
79
|
+
function fill(text, values) {
|
|
80
|
+
return String(text || "").replace(/\{(\w+)\}/g, (m, k) => (values && values[k] !== undefined ? values[k] : m));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function errorText(err) {
|
|
84
|
+
const d = err && err.response && err.response.data && err.response.data.detail;
|
|
85
|
+
return typeof d === "string" ? d : (err && err.message) || String(err);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// api: axios instance with the app's base URL (/api).
|
|
89
|
+
function selftestApi(api) {
|
|
90
|
+
const data = (p) => p.then((r) => r.data);
|
|
91
|
+
return {
|
|
92
|
+
health: () => data(api.get("/e2e/health")),
|
|
93
|
+
runs: () => data(api.get("/e2e/runs")),
|
|
94
|
+
progress: () => data(api.get("/e2e/progress")),
|
|
95
|
+
setConfig: (cfg) => data(api.post("/e2e/config", cfg)),
|
|
96
|
+
run: () => data(api.post("/e2e/run")),
|
|
97
|
+
cancel: () => data(api.post("/e2e/cancel")),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function fmtTime(iso, lang) {
|
|
102
|
+
if (!iso) return "";
|
|
103
|
+
try {
|
|
104
|
+
return new Date(iso).toLocaleString(lang === "en" ? "en-GB" : "de-CH", { dateStyle: "short", timeStyle: "short" });
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return iso;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function Verdict({ verdict, L }) {
|
|
111
|
+
return h("span", { className: `scalio-badge is-${TONE[verdict] || "neutral"}`, "data-testid": "selftest-verdict" },
|
|
112
|
+
(L.verdicts && L.verdicts[verdict]) || verdict || "-");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Counts({ counts }) {
|
|
116
|
+
const c = counts || {};
|
|
117
|
+
return h("span", { className: "scalio-selftest-counts" },
|
|
118
|
+
["PASS", "FAIL", "WARN", "SKIP"].map((k) => h("span", { key: k, className: `is-${TONE[k]}` }, `${k} ${c[k] || 0}`)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function groupBySection(results) {
|
|
122
|
+
const out = [];
|
|
123
|
+
const index = {};
|
|
124
|
+
(results || []).forEach((r) => {
|
|
125
|
+
if (!(r.section in index)) {
|
|
126
|
+
index[r.section] = out.length;
|
|
127
|
+
out.push({ section: r.section, items: [] });
|
|
128
|
+
}
|
|
129
|
+
out[index[r.section]].items.push(r);
|
|
130
|
+
});
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ResultList({ results }) {
|
|
135
|
+
return h("div", { className: "scalio-selftest-sections" },
|
|
136
|
+
groupBySection(results).map((g) =>
|
|
137
|
+
h("div", { key: g.section, className: "scalio-selftest-section" },
|
|
138
|
+
h("div", { className: "scalio-selftest-section-title" }, g.section),
|
|
139
|
+
h("ul", null, g.items.map((r, i) =>
|
|
140
|
+
h("li", { key: r.name + i, className: `is-${TONE[r.status] || "neutral"}`, "data-testid": "selftest-result" },
|
|
141
|
+
h("span", { className: "scalio-selftest-status" }, r.status),
|
|
142
|
+
h("span", { className: "scalio-selftest-name" }, r.name),
|
|
143
|
+
r.detail ? h("span", { className: "scalio-selftest-detail" }, r.detail) : null))))));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function RunRow({ run, L, lang }) {
|
|
147
|
+
const [open, setOpen] = React.useState(false);
|
|
148
|
+
const reg = run.regression || {};
|
|
149
|
+
return h("div", { className: "scalio-selftest-run", "data-testid": "selftest-run" },
|
|
150
|
+
h("div", { className: "scalio-selftest-run-head" },
|
|
151
|
+
h(Verdict, { verdict: run.verdict, L }),
|
|
152
|
+
h("span", { className: "scalio-selftest-when" }, fmtTime(run.ranAt, lang)),
|
|
153
|
+
h("span", { className: "scalio-badge is-neutral" }, run.suite),
|
|
154
|
+
h("span", { className: "scalio-selftest-source" }, (L.sources && L.sources[run.source]) || run.source),
|
|
155
|
+
run.version ? h("span", { className: "scalio-selftest-version" }, `${run.version}${run.commit ? ` · ${String(run.commit).slice(0, 7)}` : ""}`) : null,
|
|
156
|
+
h(Counts, { counts: run.counts }),
|
|
157
|
+
run.results ? h("button", { type: "button", className: "scalio-btn is-ghost", onClick: () => setOpen(!open), "data-testid": "selftest-run-toggle" },
|
|
158
|
+
open ? L.hide : L.show) : null),
|
|
159
|
+
open ? h("div", { className: "scalio-selftest-run-body" },
|
|
160
|
+
(reg.newFailures || []).length ? h("div", { className: "scalio-notice is-error", "data-testid": "selftest-new-failures" },
|
|
161
|
+
h("strong", null, L.newFailures), h("ul", null, reg.newFailures.map((r, i) =>
|
|
162
|
+
h("li", { key: i }, `${r.section} / ${r.name}`, r.was ? ` (${fill(L.was, { s: r.was })})` : "")))) : null,
|
|
163
|
+
(reg.recovered || []).length ? h("div", { className: "scalio-notice is-ok", "data-testid": "selftest-recovered" },
|
|
164
|
+
h("strong", null, L.recovered), h("ul", null, reg.recovered.map((r, i) => h("li", { key: i }, `${r.section} / ${r.name}`)))) : null,
|
|
165
|
+
h(ResultList, { results: run.results })) : null);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Props: client (selftestApi or same shape), lang, labels, notify(text, tone), pollMs (default 3000), testId.
|
|
169
|
+
function SelfTestPanel(props) {
|
|
170
|
+
const { client, lang, notify } = props;
|
|
171
|
+
const L = selftestLabels(lang, props.labels);
|
|
172
|
+
const pollMs = props.pollMs === undefined ? 3000 : props.pollMs;
|
|
173
|
+
const [health, setHealth] = React.useState(null);
|
|
174
|
+
const [runs, setRuns] = React.useState([]);
|
|
175
|
+
const [progress, setProgress] = React.useState(null);
|
|
176
|
+
const [error, setError] = React.useState("");
|
|
177
|
+
const [cfg, setCfg] = React.useState({ suite: "", runHour: -1 });
|
|
178
|
+
const [busy, setBusy] = React.useState(false);
|
|
179
|
+
const say = (text, tone) => (notify ? notify(text, tone || "success") : null);
|
|
180
|
+
|
|
181
|
+
const load = React.useCallback(async () => {
|
|
182
|
+
try {
|
|
183
|
+
const [hh, rr, pp] = await Promise.all([client.health(), client.runs(), client.progress()]);
|
|
184
|
+
setHealth(hh);
|
|
185
|
+
setRuns(rr.runs || []);
|
|
186
|
+
setProgress(pp);
|
|
187
|
+
setCfg({ suite: hh.suite || "", runHour: hh.runHour === null || hh.runHour === undefined ? -1 : hh.runHour });
|
|
188
|
+
setError("");
|
|
189
|
+
} catch (err) {
|
|
190
|
+
setError(errorText(err));
|
|
191
|
+
}
|
|
192
|
+
}, [client]);
|
|
193
|
+
|
|
194
|
+
React.useEffect(() => { load(); }, [load]);
|
|
195
|
+
const active = !!(progress && (progress.running || progress.queued));
|
|
196
|
+
React.useEffect(() => {
|
|
197
|
+
if (!active || !pollMs) return undefined;
|
|
198
|
+
const t = setInterval(async () => {
|
|
199
|
+
try {
|
|
200
|
+
const p = await client.progress();
|
|
201
|
+
setProgress(p);
|
|
202
|
+
if (!p.running && !p.queued) load();
|
|
203
|
+
} catch (e) { /* keep the last state */ }
|
|
204
|
+
}, pollMs);
|
|
205
|
+
return () => clearInterval(t);
|
|
206
|
+
}, [active, pollMs, client, load]);
|
|
207
|
+
|
|
208
|
+
if (!health) return error ? h("div", { className: "scalio-notice is-error", role: "alert" }, error) : null;
|
|
209
|
+
const enabled = !!health.enabled;
|
|
210
|
+
|
|
211
|
+
const act = async (fn, ok) => {
|
|
212
|
+
setBusy(true);
|
|
213
|
+
try {
|
|
214
|
+
await fn();
|
|
215
|
+
if (ok) say(ok);
|
|
216
|
+
await load();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
say(errorText(err), "error");
|
|
219
|
+
} finally {
|
|
220
|
+
setBusy(false);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const done = progress ? (progress.results || []).length : 0;
|
|
225
|
+
const total = Math.max(done, (progress && progress.expectedTotal) || 0);
|
|
226
|
+
const pct = total ? Math.round((done / total) * 100) : 0;
|
|
227
|
+
const hours = [-1].concat(Array.from({ length: 24 }, (_, i) => i));
|
|
228
|
+
|
|
229
|
+
return h("section", { className: "scalio-selftest", "data-testid": props.testId || "selftest-panel" },
|
|
230
|
+
h("div", { className: "scalio-selftest-part" },
|
|
231
|
+
h("div", { className: "scalio-selftest-head" },
|
|
232
|
+
h("div", null,
|
|
233
|
+
h("div", { className: "scalio-selftest-title" }, h(Icons.Activity, { className: "scalio-icon" }), L.title,
|
|
234
|
+
health.suite ? h("span", { className: "scalio-badge is-running", "data-testid": "selftest-suite" }, health.suite) : null),
|
|
235
|
+
h("p", { className: "scalio-selftest-hint" }, enabled ? L.hint : L.off)),
|
|
236
|
+
enabled ? h("div", { className: "scalio-selftest-actions" },
|
|
237
|
+
active
|
|
238
|
+
? h("button", { type: "button", className: "scalio-btn is-danger", disabled: busy, "data-testid": "selftest-cancel",
|
|
239
|
+
onClick: () => act(() => client.cancel(), L.cancelling) }, h(Icons.X, { className: "scalio-icon" }), L.cancel)
|
|
240
|
+
: h("button", { type: "button", className: "scalio-btn is-primary", disabled: busy, "data-testid": "selftest-run-now",
|
|
241
|
+
onClick: () => act(() => client.run(), L.queued) }, h(Icons.Activity, { className: "scalio-icon" }), L.runNow)) : null),
|
|
242
|
+
active ? h("div", { className: "scalio-selftest-live", "data-testid": "selftest-live" },
|
|
243
|
+
h("div", { className: "scalio-selftest-live-head" },
|
|
244
|
+
h("span", null, progress.running ? L.running : L.waiting),
|
|
245
|
+
h("span", null, `${done}${total ? ` / ${total}` : ""} ${L.checks}`)),
|
|
246
|
+
h("div", { className: "scalio-license-bar" }, h("div", { style: { width: `${pct}%` } })),
|
|
247
|
+
progress.running ? h(ResultList, { results: (progress.results || []).slice(-8) }) : null) : null),
|
|
248
|
+
enabled ? h("div", { className: "scalio-selftest-part" },
|
|
249
|
+
h("div", { className: "scalio-selftest-sub" }, h(Icons.Clock, { className: "scalio-icon", width: 14, height: 14 }), L.schedule),
|
|
250
|
+
h("div", { className: "scalio-selftest-form" },
|
|
251
|
+
h("label", null, h("span", null, L.suite),
|
|
252
|
+
h("select", { className: "scalio-input", value: cfg.suite, "data-testid": "selftest-suite-select",
|
|
253
|
+
onChange: (e) => setCfg(Object.assign({}, cfg, { suite: e.target.value })) },
|
|
254
|
+
(health.suites || []).map((s) => h("option", { key: s, value: s }, s)))),
|
|
255
|
+
h("label", null, h("span", null, L.hour),
|
|
256
|
+
h("select", { className: "scalio-input", value: String(cfg.runHour), "data-testid": "selftest-hour-select",
|
|
257
|
+
onChange: (e) => setCfg(Object.assign({}, cfg, { runHour: Number(e.target.value) })) },
|
|
258
|
+
hours.map((x) => h("option", { key: x, value: String(x) }, x === -1 ? L.noNight : `${String(x).padStart(2, "0")}:00`)))),
|
|
259
|
+
h("button", { type: "button", className: "scalio-btn", disabled: busy, "data-testid": "selftest-save",
|
|
260
|
+
onClick: () => act(() => client.setConfig({ suite: cfg.suite, runHour: cfg.runHour }), L.saved) }, L.save)),
|
|
261
|
+
h("p", { className: "scalio-selftest-hint" }, L.safeHint)) : null,
|
|
262
|
+
h("div", { className: "scalio-selftest-part" },
|
|
263
|
+
h("div", { className: "scalio-selftest-sub" }, L.runs),
|
|
264
|
+
runs.length ? runs.map((r) => h(RunRow, { key: r.id || r.ranAt, run: r, L, lang }))
|
|
265
|
+
: h("p", { className: "scalio-selftest-hint", "data-testid": "selftest-empty" }, L.never)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Hint above the page content while the test mode is on. Props: health ({enabled, suite}) or suite, lang, labels.
|
|
269
|
+
function SelfTestBanner(props) {
|
|
270
|
+
const suite = props.suite || (props.health && props.health.enabled && props.health.suite);
|
|
271
|
+
if (!suite) return null;
|
|
272
|
+
const L = selftestLabels(props.lang, props.labels);
|
|
273
|
+
return h("div", { className: "scalio-notice is-warn scalio-selftest-banner", "data-testid": "selftest-banner", role: "status" },
|
|
274
|
+
h(Icons.Activity, { className: "scalio-icon" }), h("span", null, fill(L.banner, { suite })));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { SelfTestPanel, SelfTestBanner, selftestApi, selftestLabels, SELFTEST_LABELS: LABELS };
|