@scalio-cloud/ui 0.26.0 → 0.27.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 +21 -1
- package/nav.css +17 -2
- package/package.json +1 -1
- package/react/icons.js +2 -1
- package/react/index.js +1 -1
- package/react/labels.js +32 -0
- package/react/license.js +104 -104
- package/react/maintenance.js +106 -0
- package/react/settings.js +30 -2
package/README.md
CHANGED
|
@@ -295,7 +295,8 @@ const license = licenseApi(api);
|
|
|
295
295
|
|
|
296
296
|
// Einstellungen -> System -> Lizenz (scalio-license backend: /license, /license/addons)
|
|
297
297
|
<LicensePanel client={license} lang={lang} product="Citadel" kindLabel={licenseKindLabel}
|
|
298
|
-
featureLabel={licenseFeatureLabel} noFeatures="
|
|
298
|
+
featureLabel={licenseFeatureLabel} noFeatures={t("Drift detection only")} notify={...} confirm={confirm} />
|
|
299
|
+
// noFeatures only when the app has a better text than «Nur die Grundfunktionen»
|
|
299
300
|
```
|
|
300
301
|
|
|
301
302
|
`securityApi` reads snake_case and camelCase answers. `notify(text, tone)` gets `"success"` or `"error"`; without it
|
|
@@ -304,6 +305,25 @@ the blocks show the message themselves. `confirm` is the app's dialog (`{title,
|
|
|
304
305
|
that returns the view model described in `react/license.js`. Nexus does this: its `map` adds trial badges, seat hints, invalid
|
|
305
306
|
add-ons and read-only notes (demo mode), and passes its trial notices as `statusExtra` and the reminder settings as `children`.
|
|
306
307
|
|
|
308
|
+
Since 0.27.0 (ADR 0069) the panel shows three areas in the settings form (title and hint left, card right): «Status und
|
|
309
|
+
Nutzung», «Add-ons», «Lizenzschlüssel». `children` with `extraTitle` (and `extraHint`) becomes a fourth area.
|
|
310
|
+
|
|
311
|
+
## Gruppe System: Zurücksetzen and helpers (React)
|
|
312
|
+
|
|
313
|
+
Every app shows Einstellungen → System in the same order: Lizenz, Updates, Sicherung, Zurücksetzen (`settingsGroups`
|
|
314
|
+
sorts the group `system` that way). The reset block talks to scalio-platform `factory_reset`
|
|
315
|
+
(`GET`/`POST /api/admin/factory-reset`):
|
|
316
|
+
|
|
317
|
+
```jsx
|
|
318
|
+
import { FactoryReset, factoryResetApi, SettingsField, SettingsRow } from "@scalio-cloud/ui/react";
|
|
319
|
+
|
|
320
|
+
const reset = factoryResetApi(api); // once, outside the component
|
|
321
|
+
<FactoryReset client={reset} lang={lang} tokenKey="citadel_token" backupTo="/settings/backup" link={Link} />
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`SettingsField` (label, field, help) and `SettingsRow` (title and help left, a switch or button right; `tone="danger"`)
|
|
325
|
+
keep the fields inside a `SettingsSection` the same in every app.
|
|
326
|
+
|
|
307
327
|
## Command palette (React)
|
|
308
328
|
|
|
309
329
|
Reads the same navigation data as the sidebar. Put it once into the layout; the sidebar search button opens it.
|
package/nav.css
CHANGED
|
@@ -356,6 +356,18 @@
|
|
|
356
356
|
.scalio-settings-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; }
|
|
357
357
|
.scalio-settings-body { display: flex; flex-direction: column; }
|
|
358
358
|
.scalio-settings-section-footer { display: flex; justify-content: flex-end; gap: 0.5rem; padding-top: 0.25rem; }
|
|
359
|
+
/* Content of a settings area (ADR 0069 W1): text, help under a field, checkbox with text, a form. */
|
|
360
|
+
.scalio-settings-form { display: flex; flex-direction: column; gap: 1rem; margin: 0; }
|
|
361
|
+
.scalio-settings-text { margin: 0; font-size: 0.875rem; line-height: 1.5; }
|
|
362
|
+
.scalio-settings-help { display: block; margin: 0.25rem 0 0; font-size: 0.75rem; color: hsl(var(--muted-foreground)); line-height: 1.5; }
|
|
363
|
+
.scalio-settings-check { display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.875rem; cursor: pointer; }
|
|
364
|
+
.scalio-settings-check input { margin-top: 0.2rem; }
|
|
365
|
+
.scalio-settings-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
366
|
+
.scalio-settings-row-text { min-width: 0; font-size: 0.875rem; }
|
|
367
|
+
.scalio-settings-row-text strong { display: block; font-weight: 600; }
|
|
368
|
+
.scalio-settings-row + .scalio-settings-row { padding-top: 1rem; border-top: 1px solid hsl(var(--border)); }
|
|
369
|
+
.scalio-settings-row.is-danger strong { color: hsl(var(--destructive)); }
|
|
370
|
+
.scalio-notice .scalio-btn { margin-left: auto; flex-shrink: 0; }
|
|
359
371
|
|
|
360
372
|
/* Button variants for page blocks (base: .scalio-btn in shell.css) */
|
|
361
373
|
.scalio-btn.is-primary { border-color: hsl(var(--primary)); background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); }
|
|
@@ -363,6 +375,9 @@
|
|
|
363
375
|
.scalio-btn.is-ghost { border-color: transparent; background: transparent; color: hsl(var(--muted-foreground)); }
|
|
364
376
|
.scalio-btn.is-ghost:hover { color: hsl(var(--foreground)); background: transparent; }
|
|
365
377
|
.scalio-btn.is-danger { color: hsl(var(--destructive)); }
|
|
378
|
+
.scalio-btn.is-danger.is-solid { border-color: hsl(var(--destructive)); background: hsl(var(--destructive)); color: hsl(var(--destructive-foreground, 0 0% 100%)); }
|
|
379
|
+
.scalio-btn.is-danger.is-solid:hover { background: hsl(var(--destructive) / 0.9); }
|
|
380
|
+
.scalio-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
366
381
|
.scalio-btn.is-ghost.is-danger { color: hsl(var(--muted-foreground)); }
|
|
367
382
|
.scalio-btn.is-ghost.is-danger:hover { color: hsl(var(--destructive)); }
|
|
368
383
|
.scalio-btn:disabled { opacity: 0.6; cursor: default; }
|
|
@@ -535,13 +550,13 @@
|
|
|
535
550
|
.scalio-wizard-success-icon { display: flex; align-items: center; justify-content: center; width: 3rem; height: 3rem; border-radius: 999px; }
|
|
536
551
|
|
|
537
552
|
/* License panel (LicensePanel) */
|
|
538
|
-
.scalio-license {
|
|
553
|
+
.scalio-license { font-size: 0.875rem; } /* areas in the settings form (ADR 0069); the cards come from .scalio-profile-section-body */
|
|
539
554
|
.scalio-license-part { padding: 1.25rem; }
|
|
540
555
|
.scalio-license-part + .scalio-license-part { border-top: 1px solid hsl(var(--border)); }
|
|
541
556
|
.scalio-license-title { display: flex; align-items: center; gap: 0.5rem; font-family: 'Chivo', sans-serif; font-weight: 700; }
|
|
542
557
|
.scalio-license-title .scalio-icon { color: hsl(var(--primary)); }
|
|
543
558
|
.scalio-license-hint { margin: 0.25rem 0 0; color: hsl(var(--muted-foreground)); }
|
|
544
|
-
.scalio-license-messages { display: flex; flex-direction: column; gap: 0.25rem;
|
|
559
|
+
.scalio-license-messages { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
545
560
|
.scalio-license-messages div { display: flex; gap: 0.5rem; }
|
|
546
561
|
.scalio-license-messages .scalio-icon { flex-shrink: 0; margin-top: 0.125rem; }
|
|
547
562
|
.scalio-license-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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/icons.js
CHANGED
|
@@ -132,7 +132,8 @@ const Globe = icon([
|
|
|
132
132
|
["path", { d: "M2 12h20" }],
|
|
133
133
|
]);
|
|
134
134
|
const Clock = icon([["circle", { cx: 12, cy: 12, r: 10 }], ["path", { d: "M12 6v6l4 2" }]]);
|
|
135
|
+
const Download = icon([["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }], ["path", { d: "m7 10 5 5 5-5" }], ["path", { d: "M12 15V3" }]]);
|
|
135
136
|
|
|
136
137
|
module.exports = { Sun, Moon, Monitor, LifeBuoy, BookOpen, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
|
|
137
138
|
Smartphone, KeyRound, Trash, Copy, Puzzle, Plus, Pencil, Link2, Alert, History, CornerDownLeft,
|
|
138
|
-
Users, Settings, ScrollText, Info, RadioTower, Globe, Clock };
|
|
139
|
+
Users, Settings, ScrollText, Info, RadioTower, Globe, Clock, Download };
|
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"), require("./selftest"), require("./users"), require("./roles"), require("./wizard"));
|
|
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"), require("./users"), require("./roles"), require("./wizard"), require("./maintenance"));
|
|
382
382
|
// After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
|
|
383
383
|
Object.assign(module.exports, require("./auth"));
|
package/react/labels.js
CHANGED
|
@@ -325,10 +325,26 @@ const LABELS = {
|
|
|
325
325
|
continue: "Weiter",
|
|
326
326
|
cancel: "Abbrechen",
|
|
327
327
|
},
|
|
328
|
+
reset: {
|
|
329
|
+
title: "Auf Werkseinstellungen zurücksetzen",
|
|
330
|
+
hint: "Alle Daten löschen und die Instanz neu einrichten.",
|
|
331
|
+
saas: "Diese Instanz wird im Kundenportal (Scalio Compass) zurückgesetzt: Abos, Mandant verwalten, Instanz zurücksetzen. Das Portal zeigt den neuen Einrichtungscode.",
|
|
332
|
+
text: "Löscht alle Daten dieser Instanz: Benutzer, Einstellungen und alles, was Sie erfasst haben. Die Installations-ID bleibt, die Lizenz auch, ausser Sie entfernen sie. Danach startet die Instanz wieder mit der Ersteinrichtung; den neuen Einrichtungscode finden Sie im Log des Backends.",
|
|
333
|
+
warn: "Das lässt sich nicht rückgängig machen. Sichern Sie vorher, was Sie noch brauchen.",
|
|
334
|
+
backupFirst: "Zuerst sichern",
|
|
335
|
+
confirm: "Zur Bestätigung RESET eingeben",
|
|
336
|
+
removeLicense: "Lizenz ebenfalls entfernen",
|
|
337
|
+
removeLicenseHint: "Danach läuft die Instanz als freie Edition, bis wieder ein Lizenzschlüssel eingetragen ist. Die Installations-ID bleibt.",
|
|
338
|
+
password: "Ihr Passwort",
|
|
339
|
+
run: "Auf Werkseinstellungen zurücksetzen",
|
|
340
|
+
},
|
|
328
341
|
license: {
|
|
329
342
|
title: "Lizenz",
|
|
330
343
|
hint: "Lizenzen kaufen und verlängern Sie in Scalio Compass. Der Schlüssel gilt nur für die Installations-ID unten.",
|
|
331
344
|
managed: "Die Lizenz dieser Instanz wird von Ihrem Anbieter verwaltet.",
|
|
345
|
+
statusTitle: "Status und Nutzung",
|
|
346
|
+
addonsHint: "Zusätzliche Module oder Plätze. Jedes Add-on hat einen eigenen Schlüssel aus Scalio Compass.",
|
|
347
|
+
keyHint: "Der Schlüssel aus Scalio Compass. Er gilt sofort, ohne Neustart.",
|
|
332
348
|
status: "Status",
|
|
333
349
|
active: "Aktiv",
|
|
334
350
|
limited: "Eingeschränkt",
|
|
@@ -717,10 +733,26 @@ const LABELS = {
|
|
|
717
733
|
continue: "Continue",
|
|
718
734
|
cancel: "Cancel",
|
|
719
735
|
},
|
|
736
|
+
reset: {
|
|
737
|
+
title: "Reset to factory settings",
|
|
738
|
+
hint: "Wipe all data and set the instance up again.",
|
|
739
|
+
saas: "This instance is reset in the customer portal (Scalio Compass): Subscriptions, Manage tenant, Reset instance. The portal shows the new setup code.",
|
|
740
|
+
text: "Deletes all data of this instance: users, settings and everything you created. The installation ID stays, and the license too unless you remove it. Afterwards the instance starts with the first-run setup again; the new setup code is in the log of the backend.",
|
|
741
|
+
warn: "This cannot be undone. Back up what you still need first.",
|
|
742
|
+
backupFirst: "Back up first",
|
|
743
|
+
confirm: "Type RESET to confirm",
|
|
744
|
+
removeLicense: "Remove the license as well",
|
|
745
|
+
removeLicenseHint: "Afterwards the instance runs as the free edition until a license key is entered again. The installation ID stays.",
|
|
746
|
+
password: "Your password",
|
|
747
|
+
run: "Reset to factory settings",
|
|
748
|
+
},
|
|
720
749
|
license: {
|
|
721
750
|
title: "License",
|
|
722
751
|
hint: "Buy or extend a license in Scalio Compass. The key is bound to the installation ID below.",
|
|
723
752
|
managed: "The license of this instance is managed by your provider.",
|
|
753
|
+
statusTitle: "Status and usage",
|
|
754
|
+
addonsHint: "Extra modules or seats. Every add-on has its own key from Scalio Compass.",
|
|
755
|
+
keyHint: "The key from Scalio Compass. It applies right away, no restart needed.",
|
|
724
756
|
status: "Status",
|
|
725
757
|
active: "Active",
|
|
726
758
|
limited: "Limited",
|
package/react/license.js
CHANGED
|
@@ -6,6 +6,7 @@ const React = require("react");
|
|
|
6
6
|
const h = React.createElement;
|
|
7
7
|
const Icons = require("./icons");
|
|
8
8
|
const { labelsFor } = require("./labels");
|
|
9
|
+
const { SettingsSection } = require("./settings");
|
|
9
10
|
|
|
10
11
|
function cx() {
|
|
11
12
|
return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
|
|
@@ -105,7 +106,8 @@ function UsageBar({ item }) {
|
|
|
105
106
|
// Props: client (licenseApi or the same shape), map(license, {L, lang, kindLabel, featureLabel}) -> view model,
|
|
106
107
|
// lang, labels, product ("Citadel", for the remove question), kindLabel(k), featureLabel(f), noFeatures (text when
|
|
107
108
|
// the license has no features), notify(message, tone), confirm({title, action, danger}), onChange(license), children
|
|
108
|
-
// (extra parts at the end, e.g. a policy form), formatError(err) -> text,
|
|
109
|
+
// (extra parts at the end, e.g. a policy form; extraTitle/extraHint make them an own area), formatError(err) -> text,
|
|
110
|
+
// className.
|
|
109
111
|
function LicensePanel(props) {
|
|
110
112
|
const { client, lang, notify, confirm, onChange } = props;
|
|
111
113
|
const L = labelsFor(lang, props.labels).license;
|
|
@@ -179,133 +181,131 @@ function LicensePanel(props) {
|
|
|
179
181
|
: h("dd", { className: cx(r.mono && "is-mono"), "data-testid": r.testId }, r.tone ? h("span", { className: `is-${r.tone}` }, r.value) : r.value)
|
|
180
182
|
);
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
{ className: cx("scalio-
|
|
185
|
-
h(
|
|
186
|
-
"div",
|
|
187
|
-
{ className: "scalio-license-part" },
|
|
188
|
-
h("div", { className: "scalio-license-title" }, h(Icons.ShieldCheck, { className: "scalio-icon" }), L.title),
|
|
189
|
-
h("p", { className: "scalio-license-hint" }, m.managed ? L.managed : L.hint)
|
|
190
|
-
),
|
|
191
|
-
note ? h("div", { className: cx("scalio-notice", note.tone === "error" ? "is-error" : "is-ok"), style: { margin: "1.25rem 1.25rem 0" }, role: "status" }, note.text) : null,
|
|
184
|
+
// ADR 0069 (wartung/W1): three areas in the settings form, title and hint left, card right.
|
|
185
|
+
const statusCard = [
|
|
186
|
+
note ? h("div", { key: "note", className: cx("scalio-notice", note.tone === "error" ? "is-error" : "is-ok"), role: "status" }, note.text) : null,
|
|
192
187
|
m.messages.length
|
|
193
|
-
? h("div", { className: "scalio-notice is-warn scalio-license-messages", "data-testid": "license-messages" },
|
|
188
|
+
? h("div", { key: "msg", className: "scalio-notice is-warn scalio-license-messages", "data-testid": "license-messages" },
|
|
194
189
|
m.messages.map((t) => h("div", { key: t }, h(Icons.Alert, { className: "scalio-icon" }), h("span", null, t))))
|
|
195
190
|
: null,
|
|
196
191
|
h(
|
|
197
192
|
"div",
|
|
198
|
-
{ className: "scalio-license-
|
|
193
|
+
{ key: "status", className: "scalio-license-status" },
|
|
199
194
|
h(
|
|
200
195
|
"div",
|
|
201
|
-
{ className: "scalio-license-
|
|
202
|
-
h(
|
|
203
|
-
|
|
204
|
-
{ className: "scalio-license-edition" },
|
|
205
|
-
h("span", { className: cx("scalio-badge", m.licensed ? "is-ok" : "is-warn"), "data-testid": "license-edition" }, m.edition),
|
|
206
|
-
m.customer ? h("span", { className: "scalio-license-customer" }, m.customer) : null
|
|
207
|
-
),
|
|
208
|
-
h("dl", { className: "scalio-license-rows" }, m.rows.map(row)),
|
|
209
|
-
m.features
|
|
210
|
-
? h(
|
|
211
|
-
"div",
|
|
212
|
-
null,
|
|
213
|
-
h("div", { className: "scalio-license-hint", style: { marginBottom: "0.25rem" } }, L.features),
|
|
214
|
-
m.features.length
|
|
215
|
-
? h("ul", { className: "scalio-license-features", "data-testid": "license-features" },
|
|
216
|
-
m.features.map((f) => h("li", { key: f.key }, h(Icons.Check, { className: "scalio-icon" }), f.label)))
|
|
217
|
-
: h("p", { className: "scalio-license-hint" }, props.noFeatures || L.noFeatures)
|
|
218
|
-
)
|
|
219
|
-
: null,
|
|
220
|
-
props.statusExtra || null
|
|
196
|
+
{ className: "scalio-license-edition" },
|
|
197
|
+
h("span", { className: cx("scalio-badge", m.licensed ? "is-ok" : "is-warn"), "data-testid": "license-edition" }, m.edition),
|
|
198
|
+
m.customer ? h("span", { className: "scalio-license-customer" }, m.customer) : null
|
|
221
199
|
),
|
|
222
|
-
m.
|
|
223
|
-
|
|
224
|
-
: null
|
|
225
|
-
),
|
|
226
|
-
m.addonsHidden ? null : h(
|
|
227
|
-
"div",
|
|
228
|
-
{ className: "scalio-license-part", "data-testid": "license-addons" },
|
|
229
|
-
h("div", { className: "scalio-license-sub" }, h(Icons.Puzzle, { className: "scalio-icon", width: 14, height: 14 }), L.addons),
|
|
230
|
-
m.addons.length
|
|
200
|
+
h("dl", { className: "scalio-license-rows" }, m.rows.map(row)),
|
|
201
|
+
m.features
|
|
231
202
|
? h(
|
|
232
203
|
"div",
|
|
233
|
-
|
|
234
|
-
|
|
204
|
+
null,
|
|
205
|
+
h("div", { className: "scalio-license-hint", style: { marginBottom: "0.25rem" } }, L.features),
|
|
206
|
+
m.features.length
|
|
207
|
+
? h("ul", { className: "scalio-license-features", "data-testid": "license-features" },
|
|
208
|
+
m.features.map((f) => h("li", { key: f.key }, h(Icons.Check, { className: "scalio-icon" }), f.label)))
|
|
209
|
+
: h("p", { className: "scalio-license-hint", "data-testid": "license-no-features" }, props.noFeatures || L.noFeatures)
|
|
210
|
+
)
|
|
211
|
+
: null,
|
|
212
|
+
props.statusExtra || null
|
|
213
|
+
),
|
|
214
|
+
m.usage.length
|
|
215
|
+
? h("div", { key: "usage", className: "scalio-license-usage" }, h("div", { className: "scalio-license-sub" }, L.usage), m.usage.map((u) => h(UsageBar, { key: u.key, item: u })))
|
|
216
|
+
: null,
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const addonCard = [
|
|
220
|
+
m.addons.length
|
|
221
|
+
? h(
|
|
222
|
+
"div",
|
|
223
|
+
{ key: "list", className: "scalio-license-addons" },
|
|
224
|
+
m.addons.map((a) =>
|
|
225
|
+
h(
|
|
226
|
+
"div",
|
|
227
|
+
{ key: a.id, className: cx("scalio-license-addon", a.tone && `tone-${a.tone}`), "data-testid": `addon-card-${a.id}` },
|
|
235
228
|
h(
|
|
236
229
|
"div",
|
|
237
|
-
{
|
|
230
|
+
{ style: { minWidth: 0 } },
|
|
238
231
|
h(
|
|
239
232
|
"div",
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{ className: "scalio-
|
|
244
|
-
a.name,
|
|
245
|
-
(a.badges || []).map((b) =>
|
|
246
|
-
h("span", { key: b.label, className: cx("scalio-badge", b.tone && `is-${b.tone}`), "data-testid": b.testId }, b.label))
|
|
247
|
-
),
|
|
248
|
-
h("p", { className: "scalio-license-addon-detail" },
|
|
249
|
-
a.detail !== undefined
|
|
250
|
-
? a.detail
|
|
251
|
-
: [a.seats ? `+${a.seats}` : h("span", { key: "i", className: "is-ok" }, L.included), a.exp ? ` · ${L.maintenanceUntil} ${a.exp}` : ""])
|
|
233
|
+
{ className: "scalio-license-addon-name" },
|
|
234
|
+
a.name,
|
|
235
|
+
(a.badges || []).map((b) =>
|
|
236
|
+
h("span", { key: b.label, className: cx("scalio-badge", b.tone && `is-${b.tone}`), "data-testid": b.testId }, b.label))
|
|
252
237
|
),
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
238
|
+
h("p", { className: "scalio-license-addon-detail" },
|
|
239
|
+
a.detail !== undefined
|
|
240
|
+
? a.detail
|
|
241
|
+
: [a.seats ? `+${a.seats}` : h("span", { key: "i", className: "is-ok" }, L.included), a.exp ? ` · ${L.maintenanceUntil} ${a.exp}` : ""])
|
|
242
|
+
),
|
|
243
|
+
m.managed || m.addonsReadOnly || a.removable === false
|
|
244
|
+
? null
|
|
245
|
+
: h("button", { type: "button", className: "scalio-btn is-ghost is-danger", disabled: busy, title: L.remove, "aria-label": L.remove,
|
|
246
|
+
"data-testid": `addon-remove-${a.id}`, onClick: () => run(() => client.removeAddon(a), L.addonRemoved) },
|
|
247
|
+
h(Icons.Trash, { className: "scalio-icon" }))
|
|
259
248
|
)
|
|
260
249
|
)
|
|
261
|
-
: h("p", { className: "scalio-license-empty" }, L.noAddons),
|
|
262
|
-
m.addonErrors.map((e) =>
|
|
263
|
-
h(
|
|
264
|
-
"div",
|
|
265
|
-
{ key: e.key, className: "scalio-license-addon-error", "data-testid": e.testId },
|
|
266
|
-
h("span", null, e.error),
|
|
267
|
-
e.addon && !m.managed && !m.addonsReadOnly
|
|
268
|
-
? h("button", { type: "button", className: "scalio-license-link", disabled: busy, "data-testid": `addon-remove-${e.addon.id}`,
|
|
269
|
-
onClick: () => run(() => client.removeAddon(e.addon), L.addonRemoved) }, L.remove)
|
|
270
|
-
: null
|
|
271
250
|
)
|
|
272
|
-
),
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
h("button", { type: "button", className: "scalio-btn is-primary", disabled: busy || !addon.trim(), onClick: addAddon, "data-testid": "license-addon-add" },
|
|
284
|
-
h(Icons.Plus, { className: "scalio-icon" }), L.add))
|
|
285
|
-
)
|
|
251
|
+
: h("p", { key: "empty", className: "scalio-license-empty" }, L.noAddons),
|
|
252
|
+
m.addonErrors.map((e) =>
|
|
253
|
+
h(
|
|
254
|
+
"div",
|
|
255
|
+
{ key: `err-${e.key}`, className: "scalio-license-addon-error", "data-testid": e.testId },
|
|
256
|
+
h("span", null, e.error),
|
|
257
|
+
e.addon && !m.managed && !m.addonsReadOnly
|
|
258
|
+
? h("button", { type: "button", className: "scalio-license-link", disabled: busy, "data-testid": `addon-remove-${e.addon.id}`,
|
|
259
|
+
onClick: () => run(() => client.removeAddon(e.addon), L.addonRemoved) }, L.remove)
|
|
260
|
+
: null
|
|
261
|
+
)
|
|
286
262
|
),
|
|
287
|
-
m.
|
|
288
|
-
? h("div", {
|
|
263
|
+
m.addonsReadOnly
|
|
264
|
+
? h("div", { key: "ro", className: "scalio-notice is-info", "data-testid": "license-addon-readonly" }, m.addonsReadOnly)
|
|
289
265
|
: m.managed
|
|
290
266
|
? null
|
|
291
267
|
: h(
|
|
292
268
|
"div",
|
|
293
|
-
{ className: "scalio-license-
|
|
294
|
-
h("
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{ className: "scalio-license-actions" },
|
|
300
|
-
h("button", { type: "button", className: "scalio-btn is-primary", disabled: busy || !key.trim(), onClick: activate, "data-testid": "license-activate" },
|
|
301
|
-
h(Icons.ShieldCheck, { className: "scalio-icon" }), L.activate),
|
|
302
|
-
m.keyPresent
|
|
303
|
-
? h("button", { type: "button", className: "scalio-btn is-danger", disabled: busy, onClick: remove, "data-testid": "license-remove" },
|
|
304
|
-
h(Icons.Trash, { className: "scalio-icon" }), L.remove)
|
|
305
|
-
: null
|
|
306
|
-
)
|
|
269
|
+
{ key: "form", className: "scalio-license-form" },
|
|
270
|
+
h("textarea", { className: "scalio-input scalio-textarea", rows: 2, value: addon, placeholder: L.addonPlaceholder, disabled: busy,
|
|
271
|
+
"aria-label": L.addonPlaceholder, "data-testid": "license-addon-input", onChange: (e) => setAddon(e.target.value) }),
|
|
272
|
+
h("div", { className: "scalio-license-actions" },
|
|
273
|
+
h("button", { type: "button", className: "scalio-btn is-primary", disabled: busy || !addon.trim(), onClick: addAddon, "data-testid": "license-addon-add" },
|
|
274
|
+
h(Icons.Plus, { className: "scalio-icon" }), L.add))
|
|
307
275
|
),
|
|
308
|
-
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const keyCard = m.keyReadOnly
|
|
279
|
+
? h("div", { className: "scalio-notice is-info", "data-testid": "license-key-readonly" }, m.keyReadOnly)
|
|
280
|
+
: h(
|
|
281
|
+
"div",
|
|
282
|
+
{ className: "scalio-license-form" },
|
|
283
|
+
h("textarea", { className: "scalio-input scalio-textarea", rows: 3, value: key, placeholder: L.keyPlaceholder, disabled: busy,
|
|
284
|
+
"aria-label": L.keyPlaceholder, "data-testid": "license-key-input", onChange: (e) => setKey(e.target.value) }),
|
|
285
|
+
h(
|
|
286
|
+
"div",
|
|
287
|
+
{ className: "scalio-license-actions" },
|
|
288
|
+
h("button", { type: "button", className: "scalio-btn is-primary", disabled: busy || !key.trim(), onClick: activate, "data-testid": "license-activate" },
|
|
289
|
+
h(Icons.ShieldCheck, { className: "scalio-icon" }), L.activate),
|
|
290
|
+
m.keyPresent
|
|
291
|
+
? h("button", { type: "button", className: "scalio-btn is-danger", disabled: busy, onClick: remove, "data-testid": "license-remove" },
|
|
292
|
+
h(Icons.Trash, { className: "scalio-icon" }), L.remove)
|
|
293
|
+
: null
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return h(
|
|
298
|
+
"div",
|
|
299
|
+
{ className: cx("scalio-license", props.className), "data-testid": props.testId || "license-panel" },
|
|
300
|
+
h(SettingsSection, { id: "license-status", title: L.statusTitle, description: m.managed ? L.managed : L.hint, testId: "license-status-section" }, statusCard),
|
|
301
|
+
m.addonsHidden ? null : h(SettingsSection, { id: "license-addons", title: L.addons, description: L.addonsHint, testId: "license-addons" }, addonCard),
|
|
302
|
+
m.managed ? null : h(SettingsSection, { id: "license-key", title: L.key, description: L.keyHint, testId: "license-key-section" }, keyCard),
|
|
303
|
+
// Extra parts of an app (Nexus: reminders): with extraTitle as an own area, else as they are.
|
|
304
|
+
props.children
|
|
305
|
+
? props.extraTitle
|
|
306
|
+
? h(SettingsSection, { id: "license-extra", title: props.extraTitle, description: props.extraHint, testId: "license-extra" }, props.children)
|
|
307
|
+
: props.children
|
|
308
|
+
: null
|
|
309
309
|
);
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Einstellungen -> System (ADR 0069, wartung/W2): blocks every app shows the same way. Today: Zurücksetzen (W7, after
|
|
3
|
+
// Citadel). Backend: scalio-platform `factory_reset` (GET/POST /api/admin/factory-reset).
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
const Icons = require("./icons");
|
|
7
|
+
const { labelsFor } = require("./labels");
|
|
8
|
+
const { SettingsSection, SettingsField: Field } = require("./settings");
|
|
9
|
+
|
|
10
|
+
function errorText(err) {
|
|
11
|
+
const d = err && err.response && err.response.data && err.response.data.detail;
|
|
12
|
+
if (typeof d === "string") return d;
|
|
13
|
+
if (Array.isArray(d)) return d.map((x) => (x && x.msg) || String(x)).join(" ");
|
|
14
|
+
return (err && err.message) || String(err);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// api: axios instance with the app's base URL (/api).
|
|
18
|
+
function factoryResetApi(api) {
|
|
19
|
+
return {
|
|
20
|
+
info: () => api.get("/admin/factory-reset").then((r) => r.data),
|
|
21
|
+
run: (body) => api.post("/admin/factory-reset", body).then((r) => r.data),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Props: client (factoryResetApi), lang, labels, tokenKey (localStorage key of the app's token, removed after the
|
|
26
|
+
// reset), onDone() (default: go to /login), backupTo (address of Einstellungen -> Sicherung; shows «Zuerst sichern»),
|
|
27
|
+
// link (the router's Link for backupTo), notify(message, tone), formatError(err).
|
|
28
|
+
function FactoryReset(props) {
|
|
29
|
+
const { client, lang } = props;
|
|
30
|
+
const L = labelsFor(lang, props.labels).reset;
|
|
31
|
+
const fmtErr = props.formatError || errorText;
|
|
32
|
+
const [info, setInfo] = React.useState(null);
|
|
33
|
+
const [confirm, setConfirm] = React.useState("");
|
|
34
|
+
const [password, setPassword] = React.useState("");
|
|
35
|
+
const [removeLicense, setRemoveLicense] = React.useState(false);
|
|
36
|
+
const [busy, setBusy] = React.useState(false);
|
|
37
|
+
const [error, setError] = React.useState("");
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
let alive = true;
|
|
41
|
+
client.info().then((d) => alive && setInfo(d)).catch(() => alive && setInfo({ available: false }));
|
|
42
|
+
return () => { alive = false; };
|
|
43
|
+
}, [client]);
|
|
44
|
+
|
|
45
|
+
const ready = confirm.trim() === "RESET" && (!info || !info.password || !!password);
|
|
46
|
+
const run = async (e) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (!ready) return;
|
|
49
|
+
setBusy(true);
|
|
50
|
+
setError("");
|
|
51
|
+
try {
|
|
52
|
+
await client.run({ confirm, password, remove_license: removeLicense });
|
|
53
|
+
try { if (props.tokenKey) window.localStorage.removeItem(props.tokenKey); } catch (x) { /* private mode */ }
|
|
54
|
+
if (props.onDone) props.onDone();
|
|
55
|
+
else window.location.href = "/login";
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const text = fmtErr(err);
|
|
58
|
+
if (props.notify) props.notify(text, "error");
|
|
59
|
+
else setError(text);
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const backup = props.backupTo
|
|
65
|
+
? h(props.link || "a", Object.assign({ className: "scalio-btn", "data-testid": "factory-reset-backup" },
|
|
66
|
+
props.link ? { to: props.backupTo } : { href: props.backupTo }), h(Icons.Download, { className: "scalio-icon" }), L.backupFirst)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
return h(
|
|
70
|
+
SettingsSection,
|
|
71
|
+
{ id: "reset", title: L.title, description: L.hint, testId: "factory-reset" },
|
|
72
|
+
info && !info.available
|
|
73
|
+
? h("p", { className: "scalio-settings-text", "data-testid": "factory-reset-saas" }, L.saas)
|
|
74
|
+
: null,
|
|
75
|
+
info && info.available
|
|
76
|
+
? h(
|
|
77
|
+
"form",
|
|
78
|
+
{ className: "scalio-settings-form", onSubmit: run },
|
|
79
|
+
h("p", { className: "scalio-settings-text" }, props.text || L.text),
|
|
80
|
+
h("div", { className: "scalio-notice is-warn", "data-testid": "factory-reset-warn" },
|
|
81
|
+
h("span", null, L.warn), backup),
|
|
82
|
+
h(Field, { id: "factory-reset-confirm", label: L.confirm },
|
|
83
|
+
h("input", { id: "factory-reset-confirm", className: "scalio-input", value: confirm, autoComplete: "off", disabled: busy,
|
|
84
|
+
"data-testid": "factory-reset-confirm", onChange: (e) => setConfirm(e.target.value) })),
|
|
85
|
+
h(
|
|
86
|
+
"label",
|
|
87
|
+
{ className: "scalio-settings-check" },
|
|
88
|
+
h("input", { type: "checkbox", checked: removeLicense, disabled: busy, "data-testid": "factory-reset-remove-license",
|
|
89
|
+
onChange: (e) => setRemoveLicense(e.target.checked) }),
|
|
90
|
+
h("span", null, L.removeLicense, h("span", { className: "scalio-settings-help" }, L.removeLicenseHint))
|
|
91
|
+
),
|
|
92
|
+
info.password
|
|
93
|
+
? h(Field, { id: "factory-reset-password", label: L.password },
|
|
94
|
+
h("input", { id: "factory-reset-password", type: "password", className: "scalio-input", value: password, disabled: busy,
|
|
95
|
+
autoComplete: "current-password", "data-testid": "factory-reset-password", onChange: (e) => setPassword(e.target.value) }))
|
|
96
|
+
: null,
|
|
97
|
+
error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "factory-reset-error" }, error) : null,
|
|
98
|
+
h("div", { className: "scalio-settings-section-footer" },
|
|
99
|
+
h("button", { type: "submit", className: "scalio-btn is-danger is-solid", disabled: busy || !ready, "data-testid": "factory-reset-run" },
|
|
100
|
+
h(Icons.Trash, { className: "scalio-icon" }), L.run))
|
|
101
|
+
)
|
|
102
|
+
: null
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { FactoryReset, factoryResetApi };
|
package/react/settings.js
CHANGED
|
@@ -23,12 +23,19 @@ function orderGroups(groups) {
|
|
|
23
23
|
return pick(FIXED_HEAD).concat(list.filter((g) => fixed.indexOf(g.id) < 0), pick(FIXED_TAIL));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Gruppe System is the same in every app (ADR 0069 W2): Lizenz, Updates, Sicherung, Zurücksetzen, then the rest.
|
|
27
|
+
const SYSTEM_ORDER = ["license", "updates", "backup", "reset"];
|
|
28
|
+
function systemFirst(items) {
|
|
29
|
+
const rank = (i) => (SYSTEM_ORDER.indexOf(i.id) < 0 ? SYSTEM_ORDER.length : SYSTEM_ORDER.indexOf(i.id));
|
|
30
|
+
return items.map((i, n) => [i, n]).sort((a, b) => rank(a[0]) - rank(b[0]) || a[1] - b[1]).map((x) => x[0]);
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
// Visible groups with their visible items, in the fixed order. Empty groups disappear.
|
|
27
34
|
function settingsGroups(groups, L) {
|
|
28
35
|
return orderGroups(groups)
|
|
29
36
|
.map((g) => Object.assign({}, g, {
|
|
30
37
|
label: g.label || (L && L.settingsGroups && L.settingsGroups[g.id]) || g.id,
|
|
31
|
-
items: (g.items || []).filter((i) => i && !i.hidden),
|
|
38
|
+
items: (g.id === "system" ? systemFirst : (x) => x)((g.items || []).filter((i) => i && !i.hidden)),
|
|
32
39
|
}))
|
|
33
40
|
.filter((g) => g.items.length > 0);
|
|
34
41
|
}
|
|
@@ -215,4 +222,25 @@ function SettingsSection({ id, title, description, children, footer, testId }) {
|
|
|
215
222
|
);
|
|
216
223
|
}
|
|
217
224
|
|
|
218
|
-
|
|
225
|
+
// A labelled field inside an area: label above, help text below (ADR 0069 W1). htmlFor ties the label to the input.
|
|
226
|
+
function SettingsField({ id, label, hint, children, testId }) {
|
|
227
|
+
return h(
|
|
228
|
+
"div",
|
|
229
|
+
{ className: "scalio-profile-field", "data-testid": testId },
|
|
230
|
+
label ? h("label", { className: "scalio-field-label", htmlFor: id }, label) : null,
|
|
231
|
+
children,
|
|
232
|
+
hint ? h("p", { className: "scalio-settings-help" }, hint) : null
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// A switch or button on the right of a text: title, help, control (e.g. the app's own Switch).
|
|
237
|
+
function SettingsRow({ title, hint, children, testId, tone }) {
|
|
238
|
+
return h(
|
|
239
|
+
"div",
|
|
240
|
+
{ className: cx("scalio-settings-row", tone && `is-${tone}`), "data-testid": testId },
|
|
241
|
+
h("div", { className: "scalio-settings-row-text" }, h("strong", null, title), hint ? h("span", { className: "scalio-settings-help" }, hint) : null),
|
|
242
|
+
children
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { SettingsLayout, SettingsSection, SettingsField, SettingsRow, settingsGroups, matchSetting, SETTINGS_GROUP_ORDER: FIXED_HEAD.concat(["<app>"], FIXED_TAIL), SYSTEM_ORDER };
|