@scalio-cloud/ui 0.24.0 → 0.25.1
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 +16 -2
- package/nav.css +26 -4
- package/package.json +1 -1
- package/react/index.js +1 -1
- package/react/labels.js +14 -0
- package/react/roles.js +384 -0
- package/react/users.js +27 -12
- package/CHANGELOG.md +0 -248
package/README.md
CHANGED
|
@@ -236,6 +236,18 @@ const users = usersApi(api); // once, outside the component
|
|
|
236
236
|
<MicrosoftConnection linked={user.microsoft} canUnlink={user.has_password} onLink={...} onUnlink={...} lang={lang} />
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
### Rollen und Microsoft Entra (`RoleAdmin`, `RoleSourceSettings`)
|
|
240
|
+
|
|
241
|
+
```jsx
|
|
242
|
+
import { RoleAdmin, rolesApi, RoleSourceSettings } from "@scalio-cloud/ui/react";
|
|
243
|
+
const roles = rolesApi(api); // Nexus: rolesApi(api, { permissions: "/permissions" })
|
|
244
|
+
<RoleAdmin client={roles} lang={lang} notify={toast} confirm={confirm} onChange={reloadUsers} />
|
|
245
|
+
<RoleSourceSettings value={source} tenants onSave={({ source, tenants }) => api.put("/users/role-source", { source, tenant_ids: tenants })} lang={lang} />
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The backend is scalio-platform `roles_router` (0.3.0). Built-in roles are read-only; own roles never get the
|
|
249
|
+
permissions the app keeps for administrators.
|
|
250
|
+
|
|
239
251
|
## Setup wizard (React)
|
|
240
252
|
|
|
241
253
|
Einrichtungsassistent after the first sign-in, the same in every app (ADR 0060, UISPEC section 3). The app writes
|
|
@@ -323,12 +335,14 @@ see `fonts/licenses/`. Icons in `react/icons.js`: Lucide, ISC license, see `lice
|
|
|
323
335
|
|
|
324
336
|
## Release
|
|
325
337
|
|
|
326
|
-
1. Change the files
|
|
338
|
+
1. Change the files and bump `version` in `package.json` (semver; tag without `v`).
|
|
327
339
|
2. Merge to `main`, then release it: in the Scalio Dev cockpit (row Scalio UI, "Freigeben" with the version) or
|
|
328
340
|
in GitHub (Actions -> publish -> Run workflow on main, or create the tag/release yourself). The `publish`
|
|
329
341
|
workflow tests and publishes that version to npmjs.com via trusted publishing (no token; npm org
|
|
330
342
|
`scalio-cloud`, trusted publisher Scalio-Cloud / scalio-ui / publish.yml) and, when started by hand or from
|
|
331
|
-
the cockpit, creates the tag and GitHub release with the CHANGELOG lines.
|
|
343
|
+
the cockpit, creates the tag and GitHub release with the CHANGELOG lines. The changelog lives in scalio-docs
|
|
344
|
+
`changelog/scalio-ui/` (ADR 0066): every change adds a file `changelog.d/<name>.md` there, and before the release a
|
|
345
|
+
docs PR runs `python3 tools/changelog.py --repo scalio-ui release <version>`.
|
|
332
346
|
3. Apps update with `yarn upgrade @scalio-cloud/ui@<version>` in their own PR.
|
|
333
347
|
|
|
334
348
|
`npm test` checks: light/dark variables match, the preset only uses defined variables, all font files exist,
|
package/nav.css
CHANGED
|
@@ -410,8 +410,9 @@
|
|
|
410
410
|
.scalio-users-table tbody tr:hover { background: hsl(var(--muted) / 0.4); }
|
|
411
411
|
.scalio-users-table tr.is-pending { background: rgb(217 119 6 / 0.05); }
|
|
412
412
|
.scalio-users-table tr.is-locked .scalio-users-who { opacity: 0.6; }
|
|
413
|
-
.scalio-users-hide-sm { display: none; }
|
|
413
|
+
.scalio-users-hide-sm, .scalio-users-hide-md { display: none; }
|
|
414
414
|
@media (min-width: 768px) { .scalio-users-hide-sm { display: table-cell; } }
|
|
415
|
+
@media (min-width: 1536px) { .scalio-users-hide-md { display: table-cell; } }
|
|
415
416
|
.scalio-users-who { display: flex; align-items: center; gap: 0.75rem; min-width: 0; }
|
|
416
417
|
.scalio-users-avatar {
|
|
417
418
|
display: inline-flex; align-items: center; justify-content: center; width: 2.25rem; height: 2.25rem; flex-shrink: 0;
|
|
@@ -419,16 +420,20 @@
|
|
|
419
420
|
}
|
|
420
421
|
.scalio-users-who-text { min-width: 0; }
|
|
421
422
|
.scalio-users-name { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; font-weight: 600; }
|
|
422
|
-
.scalio-users-mail { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: hsl(var(--muted-foreground));
|
|
423
|
+
.scalio-users-mail { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: hsl(var(--muted-foreground)); white-space: nowrap; }
|
|
424
|
+
.scalio-users-badges { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; }
|
|
423
425
|
.scalio-users-you { font-size: 10px; font-weight: 700; text-transform: uppercase; color: hsl(var(--primary)); }
|
|
424
426
|
.scalio-users-role { font-size: 0.8125rem; }
|
|
425
427
|
.scalio-users-date { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: hsl(var(--muted-foreground)); white-space: nowrap; }
|
|
426
|
-
.scalio-users-actions { text-align: right;
|
|
427
|
-
.scalio-users-actions
|
|
428
|
+
.scalio-users-actions { text-align: right; }
|
|
429
|
+
.scalio-users-actions-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.375rem; }
|
|
430
|
+
.scalio-users-actions .scalio-btn { height: 2rem; }
|
|
431
|
+
.scalio-users-actions .scalio-btn-icon { width: 2rem; }
|
|
428
432
|
.scalio-users-select { height: 2rem; padding: 0 0.5rem; font-size: 0.8125rem; width: auto; }
|
|
429
433
|
.scalio-users-empty, .scalio-users-loading { margin: 0; padding: 2rem 1rem; text-align: center; color: hsl(var(--muted-foreground)); border: 1px dashed hsl(var(--border)); border-radius: 2px; }
|
|
430
434
|
.scalio-users-field { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
431
435
|
.scalio-users-field .scalio-field-label { margin-top: 0; }
|
|
436
|
+
.scalio-dialog .scalio-input:disabled, .scalio-users .scalio-input:disabled { background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); cursor: not-allowed; }
|
|
432
437
|
.scalio-users-hint { margin: 0; font-size: 0.75rem; color: hsl(var(--muted-foreground)); }
|
|
433
438
|
.scalio-users-modes { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
434
439
|
.scalio-users-modes legend { margin-bottom: 0.25rem; }
|
|
@@ -464,6 +469,23 @@
|
|
|
464
469
|
.scalio-dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }
|
|
465
470
|
.scalio-dialog-left { margin-right: auto; }
|
|
466
471
|
|
|
472
|
+
/* Roles (RoleAdmin, RoleSourceSettings; scalio-docs concepts/app-roles.md) */
|
|
473
|
+
.scalio-roles-table th.scalio-roles-col, .scalio-roles-cell { text-align: center; }
|
|
474
|
+
.scalio-roles-table th.scalio-roles-col { min-width: 7.5rem; vertical-align: top; text-transform: none; letter-spacing: 0; }
|
|
475
|
+
.scalio-roles-table th.scalio-roles-col.is-own { background: hsl(var(--primary) / 0.05); }
|
|
476
|
+
.scalio-roles-perm { text-align: left; min-width: 14rem; }
|
|
477
|
+
.scalio-roles-name { font-size: 0.8125rem; font-weight: 700; color: hsl(var(--foreground)); }
|
|
478
|
+
.scalio-roles-meta { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.375rem; margin-top: 0.25rem; font-size: 10px; font-weight: 500; color: hsl(var(--muted-foreground)); }
|
|
479
|
+
.scalio-roles-tools { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.125rem; margin-top: 0.375rem; }
|
|
480
|
+
.scalio-roles-tool { height: 1.5rem; padding: 0 0.375rem; font-size: 10px; }
|
|
481
|
+
.scalio-roles-tool.is-danger { color: hsl(var(--destructive)); }
|
|
482
|
+
.scalio-roles-group td { padding-top: 1rem; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: hsl(var(--muted-foreground)); background: hsl(var(--muted) / 0.4); }
|
|
483
|
+
.scalio-roles-yes { color: #15803d; }
|
|
484
|
+
.dark .scalio-roles-yes { color: #4ade80; }
|
|
485
|
+
.scalio-roles-no { color: hsl(var(--muted-foreground)); }
|
|
486
|
+
.scalio-roles-lic { margin-left: 0.375rem; }
|
|
487
|
+
.scalio-roles-source { display: flex; flex-direction: column; gap: 0.875rem; padding: 1rem; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--card)); font-size: 0.875rem; }
|
|
488
|
+
|
|
467
489
|
/* Setup wizard (SetupWizard, ADR 0060): design of the Nexus wizard, first built in Kontor */
|
|
468
490
|
.scalio-wizard-root { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 1rem; background: rgb(0 0 0 / 0.5); }
|
|
469
491
|
.scalio-wizard {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
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"), require("./selftest"), require("./users"), 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"));
|
|
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
|
@@ -208,6 +208,13 @@ const LABELS = {
|
|
|
208
208
|
emergency: "Notfall-Administrator",
|
|
209
209
|
locked: "Gesperrt",
|
|
210
210
|
pending: "Wartet auf Freigabe",
|
|
211
|
+
invitedBadge: "Eingeladen",
|
|
212
|
+
inviteLinkTitle: "Einladung erstellt",
|
|
213
|
+
inviteLinkText: "Die Mail an {email} konnte nicht gesendet werden. Geben Sie diesen Link weiter; er gilt einmal.",
|
|
214
|
+
revokeInvite: "Einladung zurückziehen",
|
|
215
|
+
revokeTitle: "Einladung an {email} zurückziehen?",
|
|
216
|
+
revokeText: "Der Link in der Mail funktioniert danach nicht mehr.",
|
|
217
|
+
revoked: "Einladung zurückgezogen.",
|
|
211
218
|
approve: "Freischalten",
|
|
212
219
|
merge: "Zusammenführen",
|
|
213
220
|
edit: "Bearbeiten",
|
|
@@ -593,6 +600,13 @@ const LABELS = {
|
|
|
593
600
|
emergency: "Emergency administrator",
|
|
594
601
|
locked: "Locked",
|
|
595
602
|
pending: "Waiting for approval",
|
|
603
|
+
invitedBadge: "Invited",
|
|
604
|
+
inviteLinkTitle: "Invitation created",
|
|
605
|
+
inviteLinkText: "The mail to {email} could not be sent. Pass on this link; it works once.",
|
|
606
|
+
revokeInvite: "Withdraw invitation",
|
|
607
|
+
revokeTitle: "Withdraw the invitation to {email}?",
|
|
608
|
+
revokeText: "The link in the mail stops working.",
|
|
609
|
+
revoked: "Invitation withdrawn.",
|
|
596
610
|
approve: "Approve",
|
|
597
611
|
merge: "Merge",
|
|
598
612
|
edit: "Edit",
|
package/react/roles.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Konten und Rollen -> Rollen and -> Microsoft Entra, the same in every Scalio app (scalio-docs concepts/app-roles.md,
|
|
3
|
+
// roles/R1-R6). rolesApi(api, opts) maps the shared role API of scalio-platform `roles_router` (GET /roles/permissions,
|
|
4
|
+
// GET/POST /roles, PATCH/DELETE /roles/{id}); RoleAdmin shows "welche Rolle darf was" (permissions as rows, roles as
|
|
5
|
+
// columns), built-in roles read-only, own roles with checkboxes, add / copy / rename / delete. RoleSourceSettings is
|
|
6
|
+
// the role source (Scalio app or Entra app roles) with the Entra values and, where the app has them, the tenant IDs.
|
|
7
|
+
const React = require("react");
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
const Icons = require("./icons");
|
|
10
|
+
|
|
11
|
+
const TEXT = {
|
|
12
|
+
de: {
|
|
13
|
+
loading: "Wird geladen …",
|
|
14
|
+
permission: "Recht",
|
|
15
|
+
add: "Rolle anlegen",
|
|
16
|
+
addTitle: "Rolle anlegen",
|
|
17
|
+
name: "Name",
|
|
18
|
+
description: "Beschreibung (optional)",
|
|
19
|
+
copyFrom: "Rechte übernehmen von",
|
|
20
|
+
none: "keiner Rolle (leer beginnen)",
|
|
21
|
+
create: "Anlegen",
|
|
22
|
+
cancel: "Abbrechen",
|
|
23
|
+
save: "Speichern",
|
|
24
|
+
rename: "Umbenennen",
|
|
25
|
+
renameTitle: "Rolle umbenennen",
|
|
26
|
+
copy: "Kopieren",
|
|
27
|
+
copyOf: "Kopie von {name}",
|
|
28
|
+
remove: "Löschen",
|
|
29
|
+
removeTitle: "Rolle «{name}» löschen?",
|
|
30
|
+
removeText: "Konten mit dieser Rolle müssen vorher eine andere Rolle bekommen.",
|
|
31
|
+
builtin: "Eingebaut",
|
|
32
|
+
own: "Eigene Rolle",
|
|
33
|
+
users: "{n} Konten",
|
|
34
|
+
oneUser: "1 Konto",
|
|
35
|
+
license: "Zählt für die Lizenz",
|
|
36
|
+
licenseHint: "Rechte mit diesem Zeichen machen eine Rolle lizenzpflichtig.",
|
|
37
|
+
adminAll: "Alle Rechte, auch Einstellungen, Benutzer und Lizenz",
|
|
38
|
+
adminOnly: "Nur für Administratoren",
|
|
39
|
+
created: "Rolle «{name}» angelegt.",
|
|
40
|
+
saved: "Gespeichert.",
|
|
41
|
+
removed: "Rolle gelöscht.",
|
|
42
|
+
empty: "Noch keine eigenen Rollen. Mit «Rolle anlegen» stellen Sie eine Rolle aus einzelnen Rechten zusammen.",
|
|
43
|
+
readOnly: "Eingebaute Rollen sind fest. Für andere Rechte legen Sie eine eigene Rolle an, z. B. als Kopie.",
|
|
44
|
+
// RoleSourceSettings
|
|
45
|
+
source: "Rollen kommen aus",
|
|
46
|
+
sourceLocal: "dieser App",
|
|
47
|
+
sourceEntra: "Microsoft Entra ID (App-Rollen)",
|
|
48
|
+
hintLocal: "Sie vergeben die Rollen unter Benutzer. Werte für Entra ID, falls Sie später umstellen:",
|
|
49
|
+
hintEntra: "Ab der nächsten Anmeldung entscheiden die App-Rollen der Unternehmensanwendung «Scalio» für jedes Microsoft-Konto. Wer keine Rolle hat, kann sich nicht mehr anmelden. Eigene Rollen und lokale Konten vergeben Sie weiter hier. Werte:",
|
|
50
|
+
tenants: "Microsoft-Tenant-IDs",
|
|
51
|
+
tenantsHint: "Nur App-Rollen aus diesen Tenants zählen, durch Komma getrennt.",
|
|
52
|
+
entraConfirm: "Rollen aus Entra ID übernehmen?",
|
|
53
|
+
entraConfirmText: "Ab der nächsten Anmeldung entscheiden die App-Rollen in Entra ID für jedes Microsoft-Konto. Personen ohne Rolle können sich nicht mehr anmelden.",
|
|
54
|
+
entraAction: "Entra ID verwenden",
|
|
55
|
+
},
|
|
56
|
+
en: {
|
|
57
|
+
loading: "Loading …",
|
|
58
|
+
permission: "Permission",
|
|
59
|
+
add: "Add role",
|
|
60
|
+
addTitle: "Add role",
|
|
61
|
+
name: "Name",
|
|
62
|
+
description: "Description (optional)",
|
|
63
|
+
copyFrom: "Take permissions from",
|
|
64
|
+
none: "no role (start empty)",
|
|
65
|
+
create: "Add",
|
|
66
|
+
cancel: "Cancel",
|
|
67
|
+
save: "Save",
|
|
68
|
+
rename: "Rename",
|
|
69
|
+
renameTitle: "Rename role",
|
|
70
|
+
copy: "Copy",
|
|
71
|
+
copyOf: "Copy of {name}",
|
|
72
|
+
remove: "Delete",
|
|
73
|
+
removeTitle: "Delete the role “{name}”?",
|
|
74
|
+
removeText: "Accounts with this role need another role first.",
|
|
75
|
+
builtin: "Built-in",
|
|
76
|
+
own: "Own role",
|
|
77
|
+
users: "{n} accounts",
|
|
78
|
+
oneUser: "1 account",
|
|
79
|
+
license: "Counts for the license",
|
|
80
|
+
licenseHint: "Permissions with this mark make a role count for the license.",
|
|
81
|
+
adminAll: "Every permission, including settings, users and license",
|
|
82
|
+
adminOnly: "Administrators only",
|
|
83
|
+
created: "Role “{name}” added.",
|
|
84
|
+
saved: "Saved.",
|
|
85
|
+
removed: "Role deleted.",
|
|
86
|
+
empty: "No own roles yet. With “Add role” you put a role together from single permissions.",
|
|
87
|
+
readOnly: "Built-in roles are fixed. For other permissions add an own role, for example as a copy.",
|
|
88
|
+
source: "Roles come from",
|
|
89
|
+
sourceLocal: "this app",
|
|
90
|
+
sourceEntra: "Microsoft Entra ID (app roles)",
|
|
91
|
+
hintLocal: "You give roles under Users. Values for Entra ID, in case you switch later:",
|
|
92
|
+
hintEntra: "From the next sign-in, the app roles of the enterprise application “Scalio” decide for every Microsoft account. People without a role can no longer sign in. Own roles and local accounts are still given here. Values:",
|
|
93
|
+
tenants: "Microsoft tenant IDs",
|
|
94
|
+
tenantsHint: "Only app roles from these tenants count, separated by commas.",
|
|
95
|
+
entraConfirm: "Take roles from Entra ID?",
|
|
96
|
+
entraConfirmText: "From the next sign-in, the app roles in Entra ID decide for every Microsoft account. People without a role can no longer sign in.",
|
|
97
|
+
entraAction: "Use Entra ID",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function rolesText(lang, overrides) {
|
|
102
|
+
return Object.assign({}, TEXT[lang === "en" ? "en" : "de"], overrides || {});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cx() {
|
|
106
|
+
return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fill(text, values) {
|
|
110
|
+
return String(text || "").replace(/\{(\w+)\}/g, (m, k) => (values && values[k] !== undefined ? values[k] : m));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A text that is either a string or {de, en}.
|
|
114
|
+
function tr(value, lang) {
|
|
115
|
+
if (value && typeof value === "object") return value[lang === "en" ? "en" : "de"] || value.en || value.de || "";
|
|
116
|
+
return value || "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function errorText(err) {
|
|
120
|
+
const d = err && err.response && err.response.data && err.response.data.detail;
|
|
121
|
+
if (typeof d === "string") return d;
|
|
122
|
+
if (d && typeof d.message === "string") return d.message;
|
|
123
|
+
if (Array.isArray(d)) return d.map((x) => (x && x.msg) || String(x)).join(" ");
|
|
124
|
+
return (err && err.message) || String(err);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeRole(d, builtin) {
|
|
128
|
+
return {
|
|
129
|
+
id: String(d.id),
|
|
130
|
+
name: d.name,
|
|
131
|
+
description: d.description || "",
|
|
132
|
+
builtin: d.builtin !== undefined ? !!d.builtin : !!builtin,
|
|
133
|
+
admin: !!(d.admin || d.isAdmin),
|
|
134
|
+
permissions: Array.isArray(d.permissions) ? d.permissions : [],
|
|
135
|
+
license: !!d.license,
|
|
136
|
+
users: typeof d.users === "number" ? d.users : null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// api: axios instance with the app's base URL (/api). opts.base ("/roles"), opts.permissions (Nexus "/permissions").
|
|
141
|
+
function rolesApi(api, opts) {
|
|
142
|
+
const base = (opts && opts.base) || "/roles";
|
|
143
|
+
const permsUrl = (opts && opts.permissions) || `${base}/permissions`;
|
|
144
|
+
const one = (id) => `${base}/${encodeURIComponent(id)}`;
|
|
145
|
+
return {
|
|
146
|
+
permissions: () => api.get(permsUrl).then((r) => ({ permissions: r.data.permissions || [], groups: r.data.groups || [] })),
|
|
147
|
+
list: () => api.get(base).then((r) => [
|
|
148
|
+
...(r.data.builtin || []).map((d) => normalizeRole(d, true)),
|
|
149
|
+
...(r.data.custom || []).map((d) => normalizeRole(d, false)),
|
|
150
|
+
]),
|
|
151
|
+
create: (body) => api.post(base, body).then((r) => normalizeRole(r.data, false)),
|
|
152
|
+
update: (id, patch) => api.patch(one(id), patch).then((r) => normalizeRole(r.data, false)),
|
|
153
|
+
remove: (id) => api.delete(one(id)).then(() => true),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function ask(confirm, title, text, action, danger) {
|
|
158
|
+
if (confirm) return !!(await confirm({ title, text, action, danger: danger !== false }));
|
|
159
|
+
return typeof window !== "undefined" && window.confirm ? window.confirm(title) : true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Dialog({ title, onClose, children, testId }) {
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
const key = (e) => { if (e.key === "Escape") onClose(); };
|
|
165
|
+
document.addEventListener("keydown", key);
|
|
166
|
+
return () => document.removeEventListener("keydown", key);
|
|
167
|
+
}, [onClose]);
|
|
168
|
+
return h(
|
|
169
|
+
"div",
|
|
170
|
+
{ className: "scalio-dialog-root" },
|
|
171
|
+
h("div", { className: "scalio-dialog-backdrop", onClick: onClose }),
|
|
172
|
+
h(
|
|
173
|
+
"div",
|
|
174
|
+
{ className: "scalio-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, "data-testid": testId },
|
|
175
|
+
h(
|
|
176
|
+
"div",
|
|
177
|
+
{ className: "scalio-dialog-head" },
|
|
178
|
+
h("div", { className: "scalio-dialog-title" }, title),
|
|
179
|
+
h("button", { type: "button", className: "scalio-btn is-ghost scalio-btn-icon", onClick: onClose, "aria-label": "×", "data-testid": "dialog-close" },
|
|
180
|
+
h(Icons.X, { className: "scalio-icon" }))
|
|
181
|
+
),
|
|
182
|
+
children
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Props: client (rolesApi), lang, labels (overrides), notify(message, tone), confirm({title, text, action, danger}),
|
|
188
|
+
// formatError(err), canEdit (default true), onChange() after every change, testId.
|
|
189
|
+
function RoleAdmin(props) {
|
|
190
|
+
const lang = props.lang === "en" ? "en" : "de";
|
|
191
|
+
const L = rolesText(lang, props.labels);
|
|
192
|
+
const client = props.client;
|
|
193
|
+
const canEdit = props.canEdit !== false;
|
|
194
|
+
const [catalog, setCatalog] = React.useState(null);
|
|
195
|
+
const [roles, setRoles] = React.useState(null);
|
|
196
|
+
const [dialog, setDialog] = React.useState(null); // {mode: "add"|"rename", role}
|
|
197
|
+
const [form, setForm] = React.useState({ name: "", description: "", from: "" });
|
|
198
|
+
const [error, setError] = React.useState("");
|
|
199
|
+
const [busy, setBusy] = React.useState(false);
|
|
200
|
+
const fmt = props.formatError || errorText;
|
|
201
|
+
const say = (text, tone) => props.notify && props.notify(text, tone || "success");
|
|
202
|
+
|
|
203
|
+
const load = React.useCallback(() => {
|
|
204
|
+
return Promise.all([client.permissions(), client.list()])
|
|
205
|
+
.then(([c, r]) => { setCatalog(c); setRoles(r); })
|
|
206
|
+
.catch((e) => { setCatalog({ permissions: [], groups: [] }); setRoles([]); say(fmt(e), "error"); });
|
|
207
|
+
}, [client]);
|
|
208
|
+
React.useEffect(() => { load(); }, [load]);
|
|
209
|
+
|
|
210
|
+
const changed = () => { if (props.onChange) props.onChange(); return load(); };
|
|
211
|
+
const run = async (fn, okText, after) => {
|
|
212
|
+
setBusy(true);
|
|
213
|
+
setError("");
|
|
214
|
+
try {
|
|
215
|
+
const res = await fn();
|
|
216
|
+
if (okText) say(okText);
|
|
217
|
+
if (after) after(res);
|
|
218
|
+
await changed();
|
|
219
|
+
return true;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
if (dialog) setError(fmt(e)); else say(fmt(e), "error");
|
|
222
|
+
return false;
|
|
223
|
+
} finally {
|
|
224
|
+
setBusy(false);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (!catalog || !roles) return h("div", { className: "scalio-users-loading" }, L.loading);
|
|
229
|
+
|
|
230
|
+
const own = roles.filter((r) => !r.builtin);
|
|
231
|
+
const toggle = (role, key) => {
|
|
232
|
+
const has = role.permissions.includes(key);
|
|
233
|
+
const permissions = has ? role.permissions.filter((p) => p !== key) : [...role.permissions, key];
|
|
234
|
+
setRoles(roles.map((r) => (r.id === role.id ? Object.assign({}, r, { permissions }) : r)));
|
|
235
|
+
run(() => client.update(role.id, { permissions }), null);
|
|
236
|
+
};
|
|
237
|
+
const openAdd = (from) => {
|
|
238
|
+
setError("");
|
|
239
|
+
setForm({ name: from ? fill(L.copyOf, { name: tr(from.name, lang) }) : "", description: "", from: from ? from.id : "" });
|
|
240
|
+
setDialog({ mode: "add" });
|
|
241
|
+
};
|
|
242
|
+
const openRename = (role) => {
|
|
243
|
+
setError("");
|
|
244
|
+
setForm({ name: tr(role.name, lang), description: role.description || "", from: "" });
|
|
245
|
+
setDialog({ mode: "rename", role });
|
|
246
|
+
};
|
|
247
|
+
const close = () => { setDialog(null); setError(""); };
|
|
248
|
+
const submit = (e) => {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
const name = form.name.trim();
|
|
251
|
+
if (dialog.mode === "add") {
|
|
252
|
+
const src = roles.find((r) => r.id === form.from);
|
|
253
|
+
const permissions = src ? src.permissions : [];
|
|
254
|
+
run(() => client.create({ name, description: form.description.trim(), permissions }), fill(L.created, { name }), () => setDialog(null));
|
|
255
|
+
} else {
|
|
256
|
+
run(() => client.update(dialog.role.id, { name, description: form.description.trim() }), L.saved, () => setDialog(null));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const remove = async (role) => {
|
|
260
|
+
if (!(await ask(props.confirm, fill(L.removeTitle, { name: tr(role.name, lang) }), L.removeText, L.remove))) return;
|
|
261
|
+
run(() => client.remove(role.id), L.removed);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const groups = catalog.groups && catalog.groups.length ? catalog.groups : [""];
|
|
265
|
+
const byGroup = (g) => catalog.permissions.filter((p) => (p.group || "") === g || (!groups.includes(p.group || "") && g === groups[0]));
|
|
266
|
+
const usersText = (r) => (r.users === null ? "" : r.users === 1 ? L.oneUser : fill(L.users, { n: r.users }));
|
|
267
|
+
|
|
268
|
+
const head = h("tr", null,
|
|
269
|
+
h("th", { className: "scalio-roles-perm" }, L.permission),
|
|
270
|
+
roles.map((r) => h("th", { key: r.id, className: cx("scalio-roles-col", r.builtin ? "is-builtin" : "is-own"), "data-testid": `role-col-${r.id}` },
|
|
271
|
+
h("div", { className: "scalio-roles-name" }, tr(r.name, lang)),
|
|
272
|
+
h("div", { className: "scalio-roles-meta" },
|
|
273
|
+
h("span", null, r.builtin ? L.builtin : L.own),
|
|
274
|
+
usersText(r) ? h("span", null, usersText(r)) : null,
|
|
275
|
+
r.license ? h("span", { className: "scalio-badge is-warn", title: L.license, "data-testid": `role-license-${r.id}` }, "€") : null),
|
|
276
|
+
canEdit ? h("div", { className: "scalio-roles-tools" },
|
|
277
|
+
h("button", { type: "button", className: "scalio-btn is-ghost scalio-roles-tool", onClick: () => openAdd(r), disabled: busy, title: L.copy, "data-testid": `role-copy-${r.id}` }, L.copy),
|
|
278
|
+
!r.builtin ? h("button", { type: "button", className: "scalio-btn is-ghost scalio-roles-tool", onClick: () => openRename(r), disabled: busy, title: L.rename, "data-testid": `role-rename-${r.id}` }, L.rename) : null,
|
|
279
|
+
!r.builtin ? h("button", { type: "button", className: "scalio-btn is-ghost scalio-roles-tool is-danger", onClick: () => remove(r), disabled: busy, title: L.remove, "data-testid": `role-delete-${r.id}` }, L.remove) : null
|
|
280
|
+
) : null)));
|
|
281
|
+
|
|
282
|
+
const cell = (r, p) => {
|
|
283
|
+
const has = r.admin || r.permissions.includes(p.key);
|
|
284
|
+
if (r.builtin || !canEdit) {
|
|
285
|
+
return h("td", { key: r.id, className: "scalio-roles-cell", "data-testid": `role-cell-${r.id}-${p.key}` },
|
|
286
|
+
has ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no", "aria-label": "–" }, "–"));
|
|
287
|
+
}
|
|
288
|
+
return h("td", { key: r.id, className: "scalio-roles-cell" },
|
|
289
|
+
h("input", { type: "checkbox", checked: has, disabled: busy, onChange: () => toggle(r, p.key),
|
|
290
|
+
"aria-label": `${tr(r.name, lang)}: ${tr(p.label, lang)}`, "data-testid": `role-check-${r.id}-${p.key}` }));
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const body = [];
|
|
294
|
+
groups.forEach((g) => {
|
|
295
|
+
const rows = byGroup(g);
|
|
296
|
+
if (!rows.length) return;
|
|
297
|
+
if (g) body.push(h("tr", { key: `g-${g}`, className: "scalio-roles-group" }, h("td", { colSpan: roles.length + 1 }, g)));
|
|
298
|
+
rows.forEach((p) => body.push(h("tr", { key: p.key, "data-testid": `role-row-${p.key}` },
|
|
299
|
+
h("td", { className: "scalio-roles-perm" }, tr(p.label, lang),
|
|
300
|
+
p.license ? h("span", { className: "scalio-badge is-warn scalio-roles-lic", title: L.license }, "€") : null),
|
|
301
|
+
roles.map((r) => cell(r, p)))));
|
|
302
|
+
});
|
|
303
|
+
body.push(h("tr", { key: "admin-only", className: "scalio-roles-group" }, h("td", { colSpan: roles.length + 1 }, L.adminOnly)));
|
|
304
|
+
body.push(h("tr", { key: "admin-row" },
|
|
305
|
+
h("td", { className: "scalio-roles-perm" }, L.adminAll),
|
|
306
|
+
roles.map((r) => h("td", { key: r.id, className: "scalio-roles-cell" },
|
|
307
|
+
r.admin ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no" }, "–")))));
|
|
308
|
+
|
|
309
|
+
const dialogEl = dialog ? h(Dialog, { title: dialog.mode === "add" ? L.addTitle : L.renameTitle, onClose: close, testId: "role-dialog" },
|
|
310
|
+
h("form", { className: "scalio-dialog-body", onSubmit: submit },
|
|
311
|
+
h("div", { className: "scalio-users-field" },
|
|
312
|
+
h("label", { className: "scalio-field-label", htmlFor: "role-name" }, L.name),
|
|
313
|
+
h("input", { id: "role-name", className: "scalio-input", value: form.name, required: true, maxLength: 60, autoFocus: true,
|
|
314
|
+
onChange: (e) => setForm(Object.assign({}, form, { name: e.target.value })), "data-testid": "role-name" })),
|
|
315
|
+
h("div", { className: "scalio-users-field" },
|
|
316
|
+
h("label", { className: "scalio-field-label", htmlFor: "role-description" }, L.description),
|
|
317
|
+
h("input", { id: "role-description", className: "scalio-input", value: form.description, maxLength: 300,
|
|
318
|
+
onChange: (e) => setForm(Object.assign({}, form, { description: e.target.value })), "data-testid": "role-description" })),
|
|
319
|
+
dialog.mode === "add" ? h("div", { className: "scalio-users-field" },
|
|
320
|
+
h("label", { className: "scalio-field-label", htmlFor: "role-from" }, L.copyFrom),
|
|
321
|
+
h("select", { id: "role-from", className: "scalio-input", value: form.from, onChange: (e) => setForm(Object.assign({}, form, { from: e.target.value })), "data-testid": "role-from" },
|
|
322
|
+
h("option", { value: "" }, L.none),
|
|
323
|
+
roles.filter((r) => !r.admin).map((r) => h("option", { key: r.id, value: r.id }, tr(r.name, lang))))) : null,
|
|
324
|
+
error ? h("p", { className: "scalio-notice is-error scalio-users-error", role: "alert", "data-testid": "role-dialog-error" }, error) : null,
|
|
325
|
+
h("div", { className: "scalio-dialog-actions" },
|
|
326
|
+
h("button", { type: "button", className: "scalio-btn is-ghost", onClick: close }, L.cancel),
|
|
327
|
+
h("button", { type: "submit", className: "scalio-btn is-primary", disabled: busy || !form.name.trim(), "data-testid": "role-save" },
|
|
328
|
+
dialog.mode === "add" ? L.create : L.save)))) : null;
|
|
329
|
+
|
|
330
|
+
return h("div", { className: "scalio-users scalio-roles", "data-testid": props.testId || "role-admin" },
|
|
331
|
+
h("div", { className: "scalio-users-bar" },
|
|
332
|
+
h("p", { className: "scalio-users-hint" }, own.length ? L.readOnly : L.empty),
|
|
333
|
+
canEdit ? h("button", { type: "button", className: "scalio-btn is-primary", onClick: () => openAdd(null), disabled: busy, "data-testid": "role-add" },
|
|
334
|
+
h(Icons.Plus, { className: "scalio-icon" }), L.add) : null),
|
|
335
|
+
h("div", { className: "scalio-users-table-wrap" },
|
|
336
|
+
h("table", { className: "scalio-users-table scalio-roles-table", "data-testid": "roles-table" },
|
|
337
|
+
h("thead", null, head), h("tbody", null, body))),
|
|
338
|
+
catalog.permissions.some((p) => p.license) ? h("p", { className: "scalio-users-hint" }, h("span", { className: "scalio-badge is-warn" }, "€"), " ", L.licenseHint) : null,
|
|
339
|
+
dialogEl);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Props: value {source: "local"|"entra", values: [...], tenants?: [...]}, onSave({source, tenants}) -> Promise,
|
|
343
|
+
// tenants (show the tenant field), lang, labels, confirm, notify, formatError, disabled, testId.
|
|
344
|
+
function RoleSourceSettings(props) {
|
|
345
|
+
const lang = props.lang === "en" ? "en" : "de";
|
|
346
|
+
const L = rolesText(lang, props.labels);
|
|
347
|
+
const v = props.value || { source: "local", values: [] };
|
|
348
|
+
const [source, setSource] = React.useState(v.source || "local");
|
|
349
|
+
const [tenants, setTenants] = React.useState((v.tenants || []).join(", "));
|
|
350
|
+
const [busy, setBusy] = React.useState(false);
|
|
351
|
+
React.useEffect(() => { setSource(v.source || "local"); setTenants((v.tenants || []).join(", ")); }, [v.source, (v.tenants || []).join(",")]);
|
|
352
|
+
const dirty = source !== (v.source || "local") || (props.tenants && tenants !== (v.tenants || []).join(", "));
|
|
353
|
+
const save = async () => {
|
|
354
|
+
if (source === "entra" && v.source !== "entra" && !(await ask(props.confirm, L.entraConfirm, L.entraConfirmText, L.entraAction, false))) return;
|
|
355
|
+
setBusy(true);
|
|
356
|
+
try {
|
|
357
|
+
await props.onSave({ source, tenants: tenants.split(/[\s,;]+/).filter(Boolean) });
|
|
358
|
+
if (props.notify) props.notify(L.saved, "success");
|
|
359
|
+
} catch (e) {
|
|
360
|
+
if (props.notify) props.notify((props.formatError || errorText)(e), "error");
|
|
361
|
+
} finally {
|
|
362
|
+
setBusy(false);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const values = (v.values || []).map((x) => (typeof x === "string" ? x : x.value || x.name)).filter(Boolean);
|
|
366
|
+
return h("div", { className: "scalio-roles-source", "data-testid": props.testId || "role-source" },
|
|
367
|
+
h("div", { className: "scalio-users-field" },
|
|
368
|
+
h("label", { className: "scalio-field-label", htmlFor: "role-source-select" }, L.source),
|
|
369
|
+
h("select", { id: "role-source-select", className: "scalio-input scalio-users-select", value: source, disabled: props.disabled || busy,
|
|
370
|
+
onChange: (e) => setSource(e.target.value), "data-testid": "role-source-select" },
|
|
371
|
+
h("option", { value: "local" }, L.sourceLocal),
|
|
372
|
+
h("option", { value: "entra" }, L.sourceEntra))),
|
|
373
|
+
props.tenants ? h("div", { className: "scalio-users-field" },
|
|
374
|
+
h("label", { className: "scalio-field-label", htmlFor: "role-source-tenants" }, L.tenants),
|
|
375
|
+
h("input", { id: "role-source-tenants", className: "scalio-input", value: tenants, disabled: props.disabled || busy,
|
|
376
|
+
onChange: (e) => setTenants(e.target.value), "data-testid": "role-source-tenants" }),
|
|
377
|
+
h("p", { className: "scalio-users-hint" }, L.tenantsHint)) : null,
|
|
378
|
+
h("p", { className: "scalio-users-hint", "data-testid": "role-source-hint" }, source === "entra" ? L.hintEntra : L.hintLocal, " ",
|
|
379
|
+
values.map((x, i) => h(React.Fragment, { key: x }, i ? ", " : "", h("code", null, x)))),
|
|
380
|
+
h("div", null,
|
|
381
|
+
h("button", { type: "button", className: "scalio-btn is-primary", onClick: save, disabled: props.disabled || busy || !dirty, "data-testid": "role-source-save" }, L.save)));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = { RoleAdmin, RoleSourceSettings, rolesApi, rolesText };
|
package/react/users.js
CHANGED
|
@@ -52,6 +52,10 @@ function normalizeUser(d) {
|
|
|
52
52
|
name: pick(d, ["name"]) || "",
|
|
53
53
|
role: pick(d, ["role"]) || "",
|
|
54
54
|
pending: !!pick(d, ["access_pending", "accessPending", "pendingAccess"]),
|
|
55
|
+
// invited: the invitation mail is out, the person has not chosen a password yet. inviteOnly: the row is only an
|
|
56
|
+
// invitation (no account yet, Foundry): it can only be withdrawn.
|
|
57
|
+
invited: !!pick(d, ["invited", "invitePending"]),
|
|
58
|
+
inviteOnly: !!pick(d, ["invite_only", "inviteOnly"]),
|
|
55
59
|
microsoft: !!pick(d, ["microsoft", "entra_linked", "external"]),
|
|
56
60
|
managedBy: pick(d, ["managed_by", "roleManagedBy"]) || "",
|
|
57
61
|
hasPassword: !!pick(d, ["has_password", "hasPassword"]),
|
|
@@ -72,7 +76,9 @@ function usersApi(api, opts) {
|
|
|
72
76
|
const secret = (r) => pick(r.data, ["start_password", "startPassword"]) || "";
|
|
73
77
|
return {
|
|
74
78
|
list: () => api.get(base).then((r) => (Array.isArray(r.data) ? r.data : r.data.users || []).map(normalizeUser)),
|
|
75
|
-
|
|
79
|
+
// inviteLink: only when the invitation mail could not be sent (the admin passes the link on).
|
|
80
|
+
create: (body) => api.post(base, body).then((r) => ({ user: normalizeUser(r.data.user || r.data), startPassword: secret(r),
|
|
81
|
+
inviteLink: pick(r.data, ["invite_link", "inviteLink"]) || "" })),
|
|
76
82
|
update: (id, patch) => api.patch(one(id), patch).then((r) => normalizeUser(r.data.user || r.data)),
|
|
77
83
|
approve: (id, body) => api.post(`${one(id)}/approve`, body).then(() => true),
|
|
78
84
|
merge: (id, into) => api.post(`${one(id)}/merge`, { into }).then(() => true),
|
|
@@ -191,6 +197,7 @@ function UserAdmin(props) {
|
|
|
191
197
|
if (form.access !== null && form.access !== undefined) body.access = form.access;
|
|
192
198
|
run(() => client.create(body), null, (res) => {
|
|
193
199
|
if (res.startPassword) showSecret(res.user, res.startPassword, true);
|
|
200
|
+
else if (res.inviteLink) setDialog({ mode: "secret", user: res.user, password: res.inviteLink, link: true });
|
|
194
201
|
else {
|
|
195
202
|
say(form.signIn === "invite" ? fill(L.invited, { email: body.email }) : fill(L.created, { email: body.email }));
|
|
196
203
|
close();
|
|
@@ -216,12 +223,14 @@ function UserAdmin(props) {
|
|
|
216
223
|
removePassword: [L.removePasswordTitle, L.removePasswordText, L.removePasswordAction],
|
|
217
224
|
mfa: [L.resetMfaTitle, L.resetMfaText, L.resetMfaAction],
|
|
218
225
|
unlink: [L.unlinkTitle, L.unlinkText, L.unlinkAction],
|
|
219
|
-
remove: [L.removeTitle, L.removeText, L.removeAction]
|
|
226
|
+
remove: [L.removeTitle, L.removeText, L.removeAction],
|
|
227
|
+
revoke: [L.revokeTitle, L.revokeText, L.revokeInvite] }[kind];
|
|
220
228
|
if (!(await ask(confirm, fill(t[0], { email: u.email }), t[1], t[2], t[3]))) return;
|
|
221
229
|
if (kind === "password") return run(() => client.newPassword(u.id), null, (pw) => showSecret(u, pw, false));
|
|
222
230
|
if (kind === "removePassword") return run(() => client.removePassword(u.id), L.passwordRemoved);
|
|
223
231
|
if (kind === "mfa") return run(() => client.resetMfa(u.id), L.mfaReset);
|
|
224
232
|
if (kind === "unlink") return run(() => client.unlinkMicrosoft(u.id), L.unlinked);
|
|
233
|
+
if (kind === "revoke") return run(() => client.remove(u.id), L.revoked);
|
|
225
234
|
return run(() => client.remove(u.id), L.removed);
|
|
226
235
|
};
|
|
227
236
|
|
|
@@ -249,7 +258,7 @@ function UserAdmin(props) {
|
|
|
249
258
|
h("select", Object.assign({ className: "scalio-input scalio-users-select", value, onChange: (e) => onChange(e.target.value) }, attrs),
|
|
250
259
|
roles.map((r) => h("option", { key: r.value, value: r.value }, r.label)));
|
|
251
260
|
const errorBox = error ? h("p", { className: "scalio-notice is-error scalio-users-error", role: "alert", "data-testid": "user-dialog-error" }, error) : null;
|
|
252
|
-
const mergeTargets = (u) => rows.filter((r) => r.id !== u.id && !r.pending && !r.microsoft);
|
|
261
|
+
const mergeTargets = (u) => rows.filter((r) => r.id !== u.id && !r.pending && !r.microsoft && !r.inviteOnly);
|
|
253
262
|
|
|
254
263
|
const header = h(
|
|
255
264
|
"div",
|
|
@@ -270,7 +279,7 @@ function UserAdmin(props) {
|
|
|
270
279
|
h("thead", null, h("tr", null,
|
|
271
280
|
h("th", null, L.user), h("th", null, L.role),
|
|
272
281
|
props.accessColumn ? h("th", { className: "scalio-users-hide-sm" }, props.accessColumn.label) : null,
|
|
273
|
-
h("th", { className: "scalio-users-hide-
|
|
282
|
+
h("th", { className: "scalio-users-hide-md" }, L.lastLogin), h("th", null))),
|
|
274
283
|
h("tbody", null, shown.map((u) => h(
|
|
275
284
|
"tr",
|
|
276
285
|
{ key: u.id, "data-testid": `user-row-${u.email}`, className: cx(u.pending && "is-pending", !u.active && "is-locked") },
|
|
@@ -281,14 +290,16 @@ function UserAdmin(props) {
|
|
|
281
290
|
h("div", { className: "scalio-users-who-text" },
|
|
282
291
|
h("div", { className: "scalio-users-name" },
|
|
283
292
|
u.name || u.email,
|
|
284
|
-
u.id === me ? h("span", { className: "scalio-users-you" }, L.you) : null,
|
|
293
|
+
u.id === me ? h("span", { className: "scalio-users-you" }, L.you) : null),
|
|
294
|
+
h("div", { className: "scalio-users-mail" }, u.email),
|
|
295
|
+
h("div", { className: "scalio-users-badges" },
|
|
285
296
|
u.microsoft ? badge("running", "Microsoft", `user-ms-${u.email}`) : null,
|
|
286
297
|
u.hasPassword ? badge("neutral", L.password, `user-pw-${u.email}`) : null,
|
|
287
298
|
u.managedBy === "entra" ? badge("running", L.managedByEntra, `user-entra-${u.email}`) : null,
|
|
288
299
|
u.emergency && u.role === adminRole ? badge("error", L.emergency, `user-emergency-${u.email}`) : null,
|
|
289
300
|
!u.active && !u.pending ? badge("neutral", L.locked, `user-locked-${u.email}`) : null,
|
|
290
|
-
u.
|
|
291
|
-
|
|
301
|
+
u.invited || u.inviteOnly ? badge("warn", L.invitedBadge, `user-invited-${u.email}`) : null,
|
|
302
|
+
u.pending ? badge("warn", L.pending, `user-pending-${u.email}`) : null))
|
|
292
303
|
)),
|
|
293
304
|
h("td", null, u.pending && u.managedBy !== "entra"
|
|
294
305
|
? roleSelect(pendingRole[u.id] || props.defaultRole || (roles[0] && roles[0].value), (v) => setPendingRole(Object.assign({}, pendingRole, { [u.id]: v })),
|
|
@@ -297,9 +308,12 @@ function UserAdmin(props) {
|
|
|
297
308
|
props.accessColumn ? h("td", { className: "scalio-users-hide-sm" }, u.pending && props.accessField
|
|
298
309
|
? props.accessField(pendingAccess[u.id], (v) => setPendingAccess(Object.assign({}, pendingAccess, { [u.id]: v })), pendingRole[u.id] || props.defaultRole, u)
|
|
299
310
|
: props.accessColumn.render(u)) : null,
|
|
300
|
-
h("td", { className: "scalio-users-hide-
|
|
301
|
-
h("td", { className: "scalio-users-actions" }, u.
|
|
302
|
-
? h(
|
|
311
|
+
h("td", { className: "scalio-users-hide-md scalio-users-date" }, u.lastLogin ? fmtDate(u.lastLogin, lang) : "–"),
|
|
312
|
+
h("td", { className: "scalio-users-actions" }, u.inviteOnly
|
|
313
|
+
? h("button", { type: "button", className: "scalio-btn is-ghost is-danger", onClick: () => action(u, "revoke"), disabled: busy,
|
|
314
|
+
"data-testid": `user-revoke-${u.email}` }, L.revokeInvite)
|
|
315
|
+
: u.pending
|
|
316
|
+
? h("div", { className: "scalio-users-actions-row" },
|
|
303
317
|
h("button", { type: "button", className: "scalio-btn is-primary", onClick: () => approve(u), disabled: busy, "data-testid": `user-approve-${u.email}` }, L.approve),
|
|
304
318
|
mergeTargets(u).length ? h("button", { type: "button", className: "scalio-btn", onClick: () => openMerge(u), "data-testid": `user-merge-${u.email}` }, L.merge) : null,
|
|
305
319
|
h("button", { type: "button", className: "scalio-btn is-ghost is-danger scalio-btn-icon", onClick: () => action(u, "remove"), title: L.removeAction,
|
|
@@ -407,9 +421,10 @@ function UserAdmin(props) {
|
|
|
407
421
|
const c = typeof navigator !== "undefined" && navigator.clipboard;
|
|
408
422
|
if (c) c.writeText(dialog.password).then(() => say(L.copied), () => null);
|
|
409
423
|
};
|
|
410
|
-
|
|
424
|
+
const title = dialog.link ? L.inviteLinkTitle : dialog.created ? fill(L.createdTitle, { email: dialog.user.email }) : L.secretTitle;
|
|
425
|
+
body = h(Dialog, { title, onClose: close, testId: "user-secret-dialog" },
|
|
411
426
|
h("div", { className: "scalio-dialog-body" },
|
|
412
|
-
h("p", { className: "scalio-users-hint" }, fill(L.secretText, { email: dialog.user.email })),
|
|
427
|
+
h("p", { className: "scalio-users-hint" }, fill(dialog.link ? L.inviteLinkText : L.secretText, { email: dialog.user.email })),
|
|
413
428
|
h("div", { className: "scalio-users-secret", "data-testid": "user-start-password" }, dialog.password),
|
|
414
429
|
h("div", { className: "scalio-dialog-actions" },
|
|
415
430
|
h("button", { type: "button", className: "scalio-btn", onClick: copy, "data-testid": "user-copy-password" }, h(Icons.Copy, { className: "scalio-icon" }), L.copy),
|
package/CHANGELOG.md
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.24.0
|
|
4
|
-
Einrichtungsassistent als Standard für alle Apps (scalio-docs ADR 0060, contract `interfaces/setup-wizard.md`).
|
|
5
|
-
- New `SetupWizard`: the first-step dialog of Nexus and Kontor as a shared shell. Steps come as configuration
|
|
6
|
-
(`id`, `label`, `title`, `description`, `content`, `onNext`, `canNext`, `optional`); stepper on top, «Schritt n von
|
|
7
|
-
m», Zurück/Weiter, «Überspringen» for optional steps, errors of `onNext` keep the step open, short success message
|
|
8
|
-
at the end. Without `onClose` it cannot be closed; `wide` for steps with tables. Texts DE/EN in `labels.wizard`.
|
|
9
|
-
- `setupWizardApi(api, {base})` for `GET/PUT /api/setup/wizard` (reads snake_case and camelCase), `useSetupWizard()`
|
|
10
|
-
(loads the state, opens once by itself at the saved step while the setup is needed, saves each step, completes),
|
|
11
|
-
`openSetupWizard()` and `SetupWizardButton` for Einstellungen.
|
|
12
|
-
- Styles `.scalio-wizard*` in nav.css. No token changes, nothing changes until an app uses the wizard.
|
|
13
|
-
|
|
14
|
-
## 0.23.0
|
|
15
|
-
Benutzerverwaltung in allen Apps angleichen (scalio-docs ADR 0059, `users/B1`–`B6`, contract `interfaces/app-users.md`).
|
|
16
|
-
- New `UserAdmin` for Konten und Rollen → Benutzer, the same in every app: list with search and badges (Microsoft,
|
|
17
|
-
Passwort, Rolle aus Entra, Notfall-Administrator, Gesperrt, Wartet auf Freigabe), «Benutzer hinzufügen» (Microsoft,
|
|
18
|
-
Passwort with a start password shown once, or invitation by e-mail), «Bearbeiten» (name and e-mail only for accounts
|
|
19
|
-
without Microsoft, role, access, lock, emergency administrator) with the actions new start password, remove password,
|
|
20
|
-
reset second factor, unlink Microsoft and delete; waiting accounts: role, access, «Freischalten», «Zusammenführen».
|
|
21
|
-
The app passes roles and an optional access column/field (customers, organisation).
|
|
22
|
-
- `usersApi(api, {base})` for the shared routes (`/users`, Nexus `/admins`), reads snake_case and camelCase.
|
|
23
|
-
- `MicrosoftConnection` for the profile section «Verknüpfungen» (link or unlink one's own Microsoft account).
|
|
24
|
-
- New icons `Pencil`, `Link2`; modal dialog styles (`.scalio-dialog*`). No token changes, nothing changes until an app
|
|
25
|
-
uses the new components.
|
|
26
|
-
|
|
27
|
-
## 0.22.0
|
|
28
|
-
Login, Navigation und Banner vereinheitlichen (scalio-docs UISPEC section 3, ADR 0042 Nachtrag 2026-11-01).
|
|
29
|
-
- **Notice in the look of the test mode banner:** thin frame in the tone, 3px bar on the left, tinted background,
|
|
30
|
-
icon in the tone, text in the normal text colour (also for errors). Colours: red = `error` (Fehler), orange = `warn`
|
|
31
|
-
(wichtig), green = `ok` (gute Meldung), blue = `info` (Aktion). New aliases `important`, `success`, `action`
|
|
32
|
-
(`noticeKind`). Banners keep the full frame; stacked banners share one line. Dark mode has its own tones.
|
|
33
|
-
- `SelfTestBanner` is now a `Notice` banner (kind warn) and takes an optional `action` (e.g. link to «Tests»).
|
|
34
|
-
- **Behaviour change:** `normalizeTheme()` returns `system` when nothing (or something unknown) is stored, so every
|
|
35
|
-
app starts like the device on the first visit and on the login page (before: `light`).
|
|
36
|
-
- New `initialLang(stored, navLang)` / `browserLang()`: stored choice wins, otherwise German for a German browser and
|
|
37
|
-
English for everything else, the same in every app.
|
|
38
|
-
- **Visible change:** every `Notice` gets the new frame and colours (info notices turn from Klein blue to a lighter
|
|
39
|
-
blue, error text is no longer red, only bar and icon). No token changes.
|
|
40
|
-
|
|
41
|
-
## 0.21.0
|
|
42
|
-
Testmodus (scalio-docs ADR 0053, package scalio-selftest).
|
|
43
|
-
- `SelfTestPanel` for the page «Tests»: last runs with verdict, scope, source, version and counts; details per
|
|
44
|
-
section; «Neu fehlgeschlagen» and «Wieder in Ordnung» since the run before; «Jetzt ausführen» and «Abbrechen» with
|
|
45
|
-
live progress; schedule (scope and hour of the nightly run). `selftestApi(api)` maps the routes `/e2e/*`.
|
|
46
|
-
- `SelfTestBanner`: hint «Testmodus (SAFE)» while the test mode is on.
|
|
47
|
-
- CSS in `nav.css` (`.scalio-selftest*`).
|
|
48
|
-
- **Visible change:** only in apps that show the new page. No token changes.
|
|
49
|
-
|
|
50
|
-
## 0.20.0
|
|
51
|
-
Scalio Ideen (ADR 0050): feature requests from the apps, votes and comments, kept centrally in Compass.
|
|
52
|
-
- `supportApi(api, { ideas: true })` adds the routes of scalio-connect 0.8.0 (`ideas_router`). The support drawer then
|
|
53
|
-
gets a third tab «Ideen»: ideas of this app (most votes, newest, done), vote and take back, «Meine», idea page with
|
|
54
|
-
the answer from Scalio and comments, and «Idee vorschlagen» with similar ideas shown while typing.
|
|
55
|
-
- With ideas on, the ticket category «Wunsch» leaves the ticket form. A self-hosted installation without a connection
|
|
56
|
-
shows a link to the public page (`<portal>/ideen`), also in the link-only support view.
|
|
57
|
-
- New export `IdeasPanel`; CSS in `nav.css` (`.scalio-idea-*`, `.scalio-ideas*`).
|
|
58
|
-
- **Visible change:** only in apps that pass `{ ideas: true }`. No token changes.
|
|
59
|
-
|
|
60
|
-
## 0.19.0
|
|
61
|
-
Settings card "Scalio Beacon" for the apps (Beacon phase 3, scalio-docs `concepts/beacon-suite.md`).
|
|
62
|
-
- `BeaconConnection` with `peersApi(api)`: pair the installation with a Beacon instance (address + one-time code from
|
|
63
|
-
Beacon), show the pairing (address, version, last contact, queue), test it and disconnect. Routes from
|
|
64
|
-
scalio-connect 0.8.0 (`scalio_connect.peers.web.admin_router`).
|
|
65
|
-
- Labels `beacon*` in German and English.
|
|
66
|
-
- **Visible change:** none until an app shows the card. No token changes.
|
|
67
|
-
|
|
68
|
-
## 0.18.1
|
|
69
|
-
Page actions after Markus' feedback (2026-10-31, UISPEC section 2 "Page actions"). 0.18.0 was released before these
|
|
70
|
-
changes were merged; apps use 0.18.1.
|
|
71
|
-
- The slot is shown only when a page has actions and pushes the menu down; no reserved room, no separator line.
|
|
72
|
-
- 0.25rem above the buttons, 0.875rem from the last button to the next section heading (half the usual gap).
|
|
73
|
-
- Only Übersicht (first item of a first section without heading) is pinned; further items of that section stay in
|
|
74
|
-
the menu (Pulse: Geräte).
|
|
75
|
-
- Buttons in the slot: same height (2.25rem), one line, long labels end with "…". Labels without "Neu" (README).
|
|
76
|
-
- **Visible change:** only in apps that set `pageActions`. No token changes.
|
|
77
|
-
|
|
78
|
-
## 0.18.0
|
|
79
|
-
Page actions in the sidebar, first version (Markus 2026-10-31, reference Scalio Kontor; scalio-docs ADR 0042 Nachtrag).
|
|
80
|
-
- New `react/actions.js`: `PageActions` puts a page's actions (Neu …, Import, Zurück) into a fixed slot in the
|
|
81
|
-
sidebar right below Übersicht from 768px; on phones they stay in the page header. `ActionSlot`, `useDesktop()`.
|
|
82
|
-
- `Sidebar` gets `pageActions`: Übersicht pinned on top, then the slot (room for two buttons, line below), then the
|
|
83
|
-
sections. `shell.css`: an empty `.scalio-page-actions` is hidden. `react-dom` is an optional peer dependency.
|
|
84
|
-
- **Visible change:** none until an app sets `pageActions`. No token changes.
|
|
85
|
-
|
|
86
|
-
## 0.17.0
|
|
87
|
-
Scalio Pulse is built (scalio-docs ADR 0043, P1 started 2026-10-31).
|
|
88
|
-
- Product switcher: "Pulse" is no longer marked "bald"; it behaves like the other apps (link from `SCALIO_APP_LINKS`).
|
|
89
|
-
- `manualUrl("pulse")`: docs.scaliopulse.com / devdocs.scaliopulse.com.
|
|
90
|
-
- **Visible change:** after the upgrade, Pulse in the switcher loses the "bald" badge and is greyed out until the
|
|
91
|
-
instance has a link for it. No token changes.
|
|
92
|
-
|
|
93
|
-
## 0.16.0
|
|
94
|
-
Same profile page in every app, reference Scalio Nexus (Markus 2026-10-31, scalio-docs ADR 0042 Nachtrag).
|
|
95
|
-
- New `react/profile.js`: `ProfileIdentity` (letters, name, email · role), `ThemeChoice` (Hell, Dunkel, System as
|
|
96
|
-
option cards), `LanguageChoice` (flags, `fixed` for an instance-wide language), `TimezoneChoice` (searchable
|
|
97
|
-
list, "Automatisch" = browser), `PasswordChange` (current, new, confirm; mismatch check, error from the API).
|
|
98
|
-
- Helpers: `setUserTimezone`, `currentTz`, `browserTz`, `listTimezones`, `tzOffset` (apps format dates with
|
|
99
|
-
`currentTz()`), `applyTheme(mode)` / `resolveTheme` / `normalizeTheme` for light, dark and system.
|
|
100
|
-
- Icons `Globe`, `Clock`; labels `profile_` (DE/EN). Default security hint now names authenticator app and passkeys.
|
|
101
|
-
- **Visible change:** none until an app uses the new parts. No token changes.
|
|
102
|
-
|
|
103
|
-
## 0.15.0
|
|
104
|
-
New app Scalio Beacon (service desk, scalio-docs ADR 0044).
|
|
105
|
-
- Product switcher: entry "Beacon" (icon `RadioTower`) between Citadel and Pulse, available like the others.
|
|
106
|
-
- `manualUrl("beacon")`: docs.scaliobeacon.com / devdocs.scaliobeacon.com.
|
|
107
|
-
- **Visible change:** every app shows "Beacon" in the switcher after its upgrade (greyed out until the instance
|
|
108
|
-
has a link in `SCALIO_APP_LINKS`). No token changes.
|
|
109
|
-
|
|
110
|
-
## 0.14.0
|
|
111
|
-
UI alignment round 1, follow-up (scalio-docs ADR 0042, Markus 2026-10-30).
|
|
112
|
-
- `accountGroups({ lang, base, show })` and `accountArea(part)`: Konten und Rollen in the structure of the settings
|
|
113
|
-
page (groups Personen, Rechte, Sicherheit; one address per area, `/accounts/<area>`).
|
|
114
|
-
- `SettingsLayout` takes `root` (e.g. "Konten und Rollen") for the crumb, title fallback and nav label.
|
|
115
|
-
- `SubNav` with `value` and `onSelect` renders in-page tabs (buttons, `role="tablist"`).
|
|
116
|
-
- New CSS classes `.scalio-tabs` / `.scalio-tab` in `nav.css`: in-page tabs (shadcn Tabs) look like the sub
|
|
117
|
-
navigation (underline, like Nexus "Gruppen").
|
|
118
|
-
- **Visible change:** none until an app uses the new parts. No token changes.
|
|
119
|
-
|
|
120
|
-
## 0.13.0
|
|
121
|
-
UI alignment round 1 (scalio-docs ADR 0041).
|
|
122
|
-
- `editionBadges(license, { admin, demo, lang })` and `EditionBadges`: edition badge next to the product name from the
|
|
123
|
-
scalio-license state (Free, plan, trial with days left, demo; read-only or grace for administrators). `Brand`
|
|
124
|
-
takes `brand.badges` and shows them on the name line.
|
|
125
|
-
- `adminSection({ audit, accounts, settings })`: the sidebar section "Verwaltung" with fixed order, words and icons
|
|
126
|
-
(new inline icons `ScrollText`, `Users`, `Settings`, `Info`).
|
|
127
|
-
- `ApiStatus` / `useApiStatus(check)`: nothing while the API answers, a red badge "API nicht erreichbar" when not.
|
|
128
|
-
- `Notice` gets an icon, `action`, `onDismiss` ("Später") and `banner` (full width above the page).
|
|
129
|
-
- `accountTabs()` (Benutzer, Rollen, Microsoft Entra, Anmeldung, Organisationen), `PasswordPolicySelect` and
|
|
130
|
-
`policyAllowed()`: "nur Notfall-Administrator" and "aus" stay disabled until an emergency administrator exists.
|
|
131
|
-
- **Visible change:** existing `Notice` boxes get an icon in front and lay out as a row. Everything else only where
|
|
132
|
-
an app uses the new parts. No token changes.
|
|
133
|
-
|
|
134
|
-
## 0.12.0
|
|
135
|
-
- New manual button in `AppHeader` (prop `docsUrl`): a book icon between theme and support that opens the product
|
|
136
|
-
manual in a new tab; `DocsButton` on its own, `manualUrl(product, { stage, lang })` builds the address
|
|
137
|
-
(`devdocs.<domain>` on dev and test, `docs.<domain>` on production, demo and self-hosted, German under `/de/`).
|
|
138
|
-
**Visible change** only in apps that pass `docsUrl`; no token changes.
|
|
139
|
-
|
|
140
|
-
## 0.11.0
|
|
141
|
-
- Self-hosted support (ADR 0022, T11; needs scalio-connect 0.7.0 in the backend): `supportApi` gets `status()` and
|
|
142
|
-
`setButtonHidden(hidden)`. The support drawer asks `status()` first; a self-hosted installation without a
|
|
143
|
-
connection shows no form, only a note and the button "Zum Scalio-Portal" (`<portal>/support`). The card
|
|
144
|
-
"Support-Verbindung" gets the switch "Support-Knopf in der Kopfzeile ausblenden". **Visible change** only where
|
|
145
|
-
the backend reports `link_only` or the client has `setButtonHidden`; no token changes.
|
|
146
|
-
|
|
147
|
-
## 0.10.0
|
|
148
|
-
- `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
|
|
149
|
-
`badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
|
|
150
|
-
`addonsHidden`, `addonsReadOnly` and `keyReadOnly` (a note instead of the form) and a `copied` text per row.
|
|
151
|
-
Lets Nexus keep its trials, demo mode and badges on the shared panel. **No visible change** for apps that use
|
|
152
|
-
`mapScalioLicense`.
|
|
153
|
-
|
|
154
|
-
## 0.9.0
|
|
155
|
-
- New `CommandPalette` (⌘K / Strg+K): finds every page of the sidebar data (`nav`) plus extra `pages`, remembers the
|
|
156
|
-
last visited pages, and takes app commands as `groups` (Nexus: AI and guarded actions). Arrow keys, Enter, Esc,
|
|
157
|
-
no dialog library. Helpers `openCommandPalette()` (for the sidebar search button), `paletteShortcut(lang)`,
|
|
158
|
-
`paletteGroups`, `palettePages`. `LABELS` get the palette texts, `Icons` get History and CornerDownLeft.
|
|
159
|
-
**No visible change** until an app uses it; Nexus keeps its test ids (`command-palette-input`, `cmd-nav-*`).
|
|
160
|
-
|
|
161
|
-
## 0.8.1
|
|
162
|
-
- `SupportConnection` fills in the portal address of the instance's stage (`default_portal` from scalio-connect
|
|
163
|
-
0.5.0: dev -> portaldev, test -> portaltest, others -> portal.scaliocloud.com). **Visible change:** the address
|
|
164
|
-
field is no longer empty on the settings card. No change with older backends.
|
|
165
|
-
|
|
166
|
-
## 0.8.0
|
|
167
|
-
- New `SecurityFactors` (profile: authenticator app, backup codes, passkeys) with `securityApi(api, webauthn)`, and
|
|
168
|
-
`LicensePanel` (status, usage, add-ons, key) with `licenseApi(api)` and `mapScalioLicense`. They replace the copies
|
|
169
|
-
in Citadel, Foundry, Rosetta and Nexus. `LABELS` get `mfa` and `license` texts, `Icons` get Smartphone, KeyRound,
|
|
170
|
-
Trash, Copy, Puzzle, Plus, Alert, and `.scalio-btn` gets the variants `is-primary`, `is-ghost`, `is-danger`.
|
|
171
|
-
**No visible change** until an app uses the new blocks; then they look the same in every app.
|
|
172
|
-
|
|
173
|
-
## 0.7.0
|
|
174
|
-
- `SupportDrawer` takes `client` (from the new `supportApi(api)`) and `page`: tabs "Neue Anfrage" (category,
|
|
175
|
-
subject, description, technical details) and "Meine Tickets" (list, answers from support, reply, rating) for the
|
|
176
|
-
central support desk (ADR 0022). New settings card `SupportConnection` (portal address and one-time code, status,
|
|
177
|
-
check, disconnect). `LABELS` get the support texts. **Visible change only for apps that pass `client`**; without
|
|
178
|
-
it the drawer keeps its single form.
|
|
179
|
-
|
|
180
|
-
## 0.6.2
|
|
181
|
-
- **Visible change for apps using `SettingsLayout`:** the settings area uses the full width of the window (was at most
|
|
182
|
-
64rem), so wide screens show more cards side by side. No change for other apps.
|
|
183
|
-
|
|
184
|
-
## 0.6.1
|
|
185
|
-
- **Visible change for apps using `SettingsLayout`:** the settings page has no outer padding of its own any more;
|
|
186
|
-
the app's content area brings it (0.6.0 had both, so the page got a double margin). No change for other apps.
|
|
187
|
-
|
|
188
|
-
## 0.6.0
|
|
189
|
-
- New `SettingsLayout` and `SettingsSection` for the settings page (UISPEC "Einstellungen"): sub-navigation on
|
|
190
|
-
the left in fixed groups (Allgemein, app groups, Verbindungen, Benachrichtigungen, Sicherheit, System) with a
|
|
191
|
-
search, a select on phones, crumb and title of the area; areas look like the profile page. Below three areas
|
|
192
|
-
the sub-navigation is left out. `LABELS` gets the group names. No change for apps that do not use them.
|
|
193
|
-
|
|
194
|
-
## 0.5.2
|
|
195
|
-
- Navigation items take `match` (further paths that mark the item active), for one menu item over several pages.
|
|
196
|
-
- New `SubNav` (tabs as links on top of a page) and `WorkspaceSwitcher` (Identity / HR in Nexus). No change for
|
|
197
|
-
apps that do not use them.
|
|
198
|
-
|
|
199
|
-
## 0.5.1
|
|
200
|
-
- Phones: the context select (tenant, customer) is hidden in the header below 640px, there is no room next to
|
|
201
|
-
the header buttons. `AppShell` takes `drawerTop` and shows it above the navigation in the phone drawer.
|
|
202
|
-
|
|
203
|
-
## 0.5.0
|
|
204
|
-
- **Visible change:** `rounded-sm` is now 2px as UISPEC says (was 0px from the preset). Buttons, fields, cards and
|
|
205
|
-
badges in Nexus, Citadel and Foundry get barely visible round corners; Rosetta already had 2px and can drop its
|
|
206
|
-
own `borderRadius` override.
|
|
207
|
-
- New app frame in `@scalio-cloud/ui/react`: `AppShell`, `Sidebar` (navigation as data: sections, hidden items,
|
|
208
|
-
badges, search button, one main action, version), `NavDrawer` for phones, `ContextSelect` for tenant, customer or
|
|
209
|
-
organisation in the header.
|
|
210
|
-
- Moved here from the apps: `ProductSwitcher` with `PRODUCTS`, `switcherHref`, `stageBadge`, `scalioFeatures`,
|
|
211
|
-
`useScalioApps`, and `SupportDrawer` (the app passes the send function). Same test ids as before.
|
|
212
|
-
- Page blocks: `KpiTile`, `EmptyState`, `Loading`, `Notice`, `StatusBadge`; `PageHeader` takes `back`.
|
|
213
|
-
- `LABELS` gets the shared navigation words (Übersicht, Verwaltung, Protokoll, Konten und Rollen, Einstellungen, ...).
|
|
214
|
-
- New `nav.css` (imported by `index.css`). No other visual change for apps that do not use the new parts.
|
|
215
|
-
|
|
216
|
-
## 0.4.1
|
|
217
|
-
- No change to tokens, styles or components. CI and publish workflow on current action versions (past Node 20).
|
|
218
|
-
|
|
219
|
-
## 0.4.0
|
|
220
|
-
- New sign-in building blocks in `@scalio-cloud/ui/react`: `AuthLayout` (app brand on a dark grid on the left, form
|
|
221
|
-
on the right, language and optional theme top right, small brand above the form on phones), `AuthHeader`,
|
|
222
|
-
`MicrosoftButton`, `AuthDivider` ("oder mit E-Mail"), `AuthMessage`.
|
|
223
|
-
- New `ProfileSection` and `PROFILE_SECTIONS` for the profile page (Konto, Darstellung, Sprache und Region,
|
|
224
|
-
Sicherheit, Verknüpfungen), title and hint on the left, content in a card on the right.
|
|
225
|
-
- More shared words in `LABELS` (Anmelden, Passwort vergessen?, Passwort ändern, Zwei-Faktor-Anmeldung, ...).
|
|
226
|
-
- New `auth.css` (imported by `index.css`). No visual change for apps that do not use the new components.
|
|
227
|
-
|
|
228
|
-
## 0.3.0
|
|
229
|
-
- New `@scalio-cloud/ui/react`: shared app header (`AppHeader` with status, language, theme, support and user menu
|
|
230
|
-
in a fixed order), `UserMenu`, `LanguageToggle`, `ThemeToggle`, `SupportButton`, `PageHeader`, `VersionTag`.
|
|
231
|
-
Only peer dependency is React (optional for apps that use just the tokens).
|
|
232
|
-
- New `shell.css` (imported by `index.css`): styles for these parts with `scalio-*` classes. No visual change for
|
|
233
|
-
apps that do not use the components.
|
|
234
|
-
|
|
235
|
-
## 0.2.0
|
|
236
|
-
- Dark mode colours from Scalio Rosetta: sidebar and other `bg-muted` areas are now darker than the page
|
|
237
|
-
(before: lighter grey), primary blue in dark mode `223 100% 45%` (before: brighter `223 85% 62%`), card,
|
|
238
|
-
border and input slightly adjusted. Light mode unchanged.
|
|
239
|
-
|
|
240
|
-
## 0.1.2
|
|
241
|
-
- Published via npm trusted publishing (GitHub Actions OIDC), no npm token. Content unchanged.
|
|
242
|
-
|
|
243
|
-
## 0.1.1
|
|
244
|
-
- Published publicly on npmjs.com instead of GitHub Packages (no token needed to install). Content unchanged.
|
|
245
|
-
|
|
246
|
-
## 0.1.0
|
|
247
|
-
- First version: Control Room tokens (light/dark, taken 1:1 from Scalio Nexus), Tailwind preset, self-hosted
|
|
248
|
-
fonts Chivo / IBM Plex Sans / JetBrains Mono. Compiled against Nexus it gives the same CSS as Nexus's own config.
|