@scalio-cloud/ui 0.25.1 → 0.26.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/nav.css +14 -1
- package/package.json +1 -1
- package/react/labels.js +2 -2
- package/react/roles.js +153 -32
package/nav.css
CHANGED
|
@@ -484,6 +484,19 @@
|
|
|
484
484
|
.dark .scalio-roles-yes { color: #4ade80; }
|
|
485
485
|
.scalio-roles-no { color: hsl(var(--muted-foreground)); }
|
|
486
486
|
.scalio-roles-lic { margin-left: 0.375rem; }
|
|
487
|
+
.scalio-roles-cell > svg, .scalio-roles-cell > input, .scalio-roles-cell > .scalio-roles-no { display: block; margin-left: auto; margin-right: auto; }
|
|
488
|
+
.scalio-roles-table th.scalio-roles-col.is-mine, .scalio-roles-cell.is-mine { background: hsl(var(--primary) / 0.08); }
|
|
489
|
+
.scalio-roles-table th.scalio-roles-col.is-mine .scalio-roles-name { color: hsl(var(--primary)); }
|
|
490
|
+
.scalio-roles-mine { margin-bottom: 0.25rem; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: hsl(var(--primary)); }
|
|
491
|
+
.scalio-tenant-list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; border: 1px solid hsl(var(--border)); border-radius: 2px; }
|
|
492
|
+
.scalio-tenant-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.625rem 0.875rem; border-bottom: 1px solid hsl(var(--border)); }
|
|
493
|
+
.scalio-tenant-row:last-child { border-bottom: 0; }
|
|
494
|
+
.scalio-tenant-who { min-width: 0; }
|
|
495
|
+
.scalio-tenant-domain { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-weight: 600; overflow-wrap: anywhere; }
|
|
496
|
+
.scalio-tenant-id { margin-top: 0.125rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: hsl(var(--muted-foreground)); overflow-wrap: anywhere; }
|
|
497
|
+
.scalio-tenant-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
498
|
+
.scalio-tenant-manual { display: flex; flex-direction: column; gap: 0.5rem; max-width: 28rem; }
|
|
499
|
+
.scalio-btn.is-disabled { pointer-events: none; opacity: 0.5; }
|
|
487
500
|
.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
501
|
|
|
489
502
|
/* Setup wizard (SetupWizard, ADR 0060): design of the Nexus wizard, first built in Kontor */
|
|
@@ -493,7 +506,7 @@
|
|
|
493
506
|
padding: 1.5rem; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--background)); color: hsl(var(--foreground));
|
|
494
507
|
box-shadow: 0 16px 40px rgb(0 0 0 / 0.25); font-size: 0.875rem;
|
|
495
508
|
}
|
|
496
|
-
.scalio-wizard.is-wide { max-width:
|
|
509
|
+
.scalio-wizard.is-wide { max-width: 72rem; }
|
|
497
510
|
.scalio-wizard-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
498
511
|
.scalio-wizard-title { margin: 0; font-family: 'Chivo', sans-serif; font-size: 1.5rem; font-weight: 900; line-height: 1.2; text-transform: uppercase; letter-spacing: -0.025em; }
|
|
499
512
|
.scalio-wizard-subtitle { margin: 0.25rem 0 0; color: hsl(var(--muted-foreground)); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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/labels.js
CHANGED
|
@@ -64,7 +64,7 @@ const LABELS = {
|
|
|
64
64
|
graceTip: "Diese Version liegt nach Ihrem Lizenzdatum und wechselt nach der Kulanzzeit in den Nur-Lesen-Modus.",
|
|
65
65
|
},
|
|
66
66
|
accountGroups: { people: "Personen", access: "Rechte" },
|
|
67
|
-
accountTabs: { users: "Benutzer", roles: "Rollen", entra: "
|
|
67
|
+
accountTabs: { users: "Benutzer", roles: "Rollen", entra: "Rollen aus Entra ID", signIn: "Anmeldung", organizations: "Organisationen" },
|
|
68
68
|
passwordPolicy: {
|
|
69
69
|
label: "Anmeldung mit Passwort",
|
|
70
70
|
local: "Erlaubt",
|
|
@@ -460,7 +460,7 @@ const LABELS = {
|
|
|
460
460
|
graceTip: "This version is past your license date and turns read-only after the grace period.",
|
|
461
461
|
},
|
|
462
462
|
accountGroups: { people: "People", access: "Permissions" },
|
|
463
|
-
accountTabs: { users: "Users", roles: "Roles", entra: "
|
|
463
|
+
accountTabs: { users: "Users", roles: "Roles", entra: "Roles from Entra ID", signIn: "Sign-in", organizations: "Organizations" },
|
|
464
464
|
passwordPolicy: {
|
|
465
465
|
label: "Password sign-in",
|
|
466
466
|
local: "Allowed",
|
package/react/roles.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Konten und Rollen -> Rollen and ->
|
|
2
|
+
// Konten und Rollen -> Rollen and -> Rollen aus Entra ID, the same in every Scalio app (scalio-docs concepts/app-roles.md,
|
|
3
3
|
// roles/R1-R6). rolesApi(api, opts) maps the shared role API of scalio-platform `roles_router` (GET /roles/permissions,
|
|
4
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
|
|
6
|
-
// the role source (Scalio app or Entra app roles) with the Entra values
|
|
5
|
+
// columns), built-in roles read-only, own roles with checkboxes, add / copy / rename / delete; the column of the signed-in
|
|
6
|
+
// person's role is marked. RoleSourceSettings is the role source (Scalio app or Entra app roles) with the Entra values
|
|
7
|
+
// and the list of tenants whose roles count, with «Tenant hinzufügen» by admin consent (ADR 0068).
|
|
7
8
|
const React = require("react");
|
|
8
9
|
const h = React.createElement;
|
|
9
10
|
const Icons = require("./icons");
|
|
@@ -47,8 +48,26 @@ const TEXT = {
|
|
|
47
48
|
sourceEntra: "Microsoft Entra ID (App-Rollen)",
|
|
48
49
|
hintLocal: "Sie vergeben die Rollen unter Benutzer. Werte für Entra ID, falls Sie später umstellen:",
|
|
49
50
|
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
|
-
|
|
51
|
-
|
|
51
|
+
yourRole: "Ihre Rolle",
|
|
52
|
+
tenants: "Vertraute Tenants",
|
|
53
|
+
tenantsHint: "Nur App-Rollen aus diesen Tenants zählen, und Personen aus diesen Tenants können sich anmelden.",
|
|
54
|
+
tenantsEmpty: "Noch kein Tenant. Mit «Tenant hinzufügen» gibt ein Admin des Tenants die Zustimmung für die App «Scalio».",
|
|
55
|
+
tenantAdd: "Tenant hinzufügen",
|
|
56
|
+
tenantAddHint: "Sie melden sich als Admin des Tenants bei Microsoft an und stimmen der App «Scalio» zu.",
|
|
57
|
+
tenantManual: "Von Hand eintragen",
|
|
58
|
+
tenantManualHint: "Nur wenn ein Admin des Tenants der App «Scalio» schon zugestimmt hat.",
|
|
59
|
+
tenantId: "Tenant-ID",
|
|
60
|
+
tenantAddManual: "Eintragen",
|
|
61
|
+
tenantInvalid: "Das ist keine Tenant-ID (GUID).",
|
|
62
|
+
tenantRemove: "Entfernen",
|
|
63
|
+
tenantRemoveTitle: "Tenant «{name}» entfernen?",
|
|
64
|
+
tenantRemoveText: "App-Rollen aus diesem Tenant zählen dann nicht mehr. Die Unternehmensanwendung im Tenant bleibt; entfernen Sie sie dort bei Bedarf selbst.",
|
|
65
|
+
tenantFixed: "Fest",
|
|
66
|
+
tenantAdded: "Tenant hinzugefügt.",
|
|
67
|
+
tenantRemoved: "Tenant entfernt.",
|
|
68
|
+
consentCancelled: "Die Zustimmung wurde abgebrochen.",
|
|
69
|
+
consentFailed: "Die Zustimmung hat nicht geklappt. Melden Sie sich mit einem Konto an, das in diesem Tenant Admin ist.",
|
|
70
|
+
consentForbidden: "Nur Admins dieser App können einen Tenant hinzufügen.",
|
|
52
71
|
entraConfirm: "Rollen aus Entra ID übernehmen?",
|
|
53
72
|
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
73
|
entraAction: "Entra ID verwenden",
|
|
@@ -90,8 +109,26 @@ const TEXT = {
|
|
|
90
109
|
sourceEntra: "Microsoft Entra ID (app roles)",
|
|
91
110
|
hintLocal: "You give roles under Users. Values for Entra ID, in case you switch later:",
|
|
92
111
|
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
|
-
|
|
94
|
-
|
|
112
|
+
yourRole: "Your role",
|
|
113
|
+
tenants: "Trusted tenants",
|
|
114
|
+
tenantsHint: "Only app roles from these tenants count, and people from these tenants can sign in.",
|
|
115
|
+
tenantsEmpty: "No tenant yet. With “Add tenant” an admin of the tenant consents to the app “Scalio”.",
|
|
116
|
+
tenantAdd: "Add tenant",
|
|
117
|
+
tenantAddHint: "You sign in to Microsoft as an admin of the tenant and consent to the app “Scalio”.",
|
|
118
|
+
tenantManual: "Enter by hand",
|
|
119
|
+
tenantManualHint: "Only if an admin of the tenant already consented to the app “Scalio”.",
|
|
120
|
+
tenantId: "Tenant ID",
|
|
121
|
+
tenantAddManual: "Add",
|
|
122
|
+
tenantInvalid: "This is not a tenant ID (GUID).",
|
|
123
|
+
tenantRemove: "Remove",
|
|
124
|
+
tenantRemoveTitle: "Remove the tenant “{name}”?",
|
|
125
|
+
tenantRemoveText: "App roles from this tenant no longer count. The enterprise application stays in the tenant; remove it there yourself if needed.",
|
|
126
|
+
tenantFixed: "Fixed",
|
|
127
|
+
tenantAdded: "Tenant added.",
|
|
128
|
+
tenantRemoved: "Tenant removed.",
|
|
129
|
+
consentCancelled: "The consent was cancelled.",
|
|
130
|
+
consentFailed: "The consent did not work. Sign in with an account that is an admin in this tenant.",
|
|
131
|
+
consentForbidden: "Only admins of this app can add a tenant.",
|
|
95
132
|
entraConfirm: "Take roles from Entra ID?",
|
|
96
133
|
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
134
|
entraAction: "Use Entra ID",
|
|
@@ -265,9 +302,11 @@ function RoleAdmin(props) {
|
|
|
265
302
|
const byGroup = (g) => catalog.permissions.filter((p) => (p.group || "") === g || (!groups.includes(p.group || "") && g === groups[0]));
|
|
266
303
|
const usersText = (r) => (r.users === null ? "" : r.users === 1 ? L.oneUser : fill(L.users, { n: r.users }));
|
|
267
304
|
|
|
305
|
+
const mine = (r) => props.currentRole !== undefined && props.currentRole !== null && String(props.currentRole) === r.id;
|
|
268
306
|
const head = h("tr", null,
|
|
269
307
|
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}` },
|
|
308
|
+
roles.map((r) => h("th", { key: r.id, className: cx("scalio-roles-col", r.builtin ? "is-builtin" : "is-own", mine(r) && "is-mine"), "data-testid": `role-col-${r.id}` },
|
|
309
|
+
mine(r) ? h("div", { className: "scalio-roles-mine", "data-testid": "role-yours" }, L.yourRole) : null,
|
|
271
310
|
h("div", { className: "scalio-roles-name" }, tr(r.name, lang)),
|
|
272
311
|
h("div", { className: "scalio-roles-meta" },
|
|
273
312
|
h("span", null, r.builtin ? L.builtin : L.own),
|
|
@@ -282,10 +321,10 @@ function RoleAdmin(props) {
|
|
|
282
321
|
const cell = (r, p) => {
|
|
283
322
|
const has = r.admin || r.permissions.includes(p.key);
|
|
284
323
|
if (r.builtin || !canEdit) {
|
|
285
|
-
return h("td", { key: r.id, className: "scalio-roles-cell", "data-testid": `role-cell-${r.id}-${p.key}` },
|
|
324
|
+
return h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine"), "data-testid": `role-cell-${r.id}-${p.key}` },
|
|
286
325
|
has ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no", "aria-label": "–" }, "–"));
|
|
287
326
|
}
|
|
288
|
-
return h("td", { key: r.id, className: "scalio-roles-cell" },
|
|
327
|
+
return h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine") },
|
|
289
328
|
h("input", { type: "checkbox", checked: has, disabled: busy, onChange: () => toggle(r, p.key),
|
|
290
329
|
"aria-label": `${tr(r.name, lang)}: ${tr(p.label, lang)}`, "data-testid": `role-check-${r.id}-${p.key}` }));
|
|
291
330
|
};
|
|
@@ -303,7 +342,7 @@ function RoleAdmin(props) {
|
|
|
303
342
|
body.push(h("tr", { key: "admin-only", className: "scalio-roles-group" }, h("td", { colSpan: roles.length + 1 }, L.adminOnly)));
|
|
304
343
|
body.push(h("tr", { key: "admin-row" },
|
|
305
344
|
h("td", { className: "scalio-roles-perm" }, L.adminAll),
|
|
306
|
-
roles.map((r) => h("td", { key: r.id, className: "scalio-roles-cell" },
|
|
345
|
+
roles.map((r) => h("td", { key: r.id, className: cx("scalio-roles-cell", mine(r) && "is-mine") },
|
|
307
346
|
r.admin ? h(Icons.Check, { className: "scalio-icon scalio-roles-yes", "aria-label": "✓" }) : h("span", { className: "scalio-roles-no" }, "–")))));
|
|
308
347
|
|
|
309
348
|
const dialogEl = dialog ? h(Dialog, { title: dialog.mode === "add" ? L.addTitle : L.renameTitle, onClose: close, testId: "role-dialog" },
|
|
@@ -339,46 +378,128 @@ function RoleAdmin(props) {
|
|
|
339
378
|
dialogEl);
|
|
340
379
|
}
|
|
341
380
|
|
|
342
|
-
|
|
343
|
-
|
|
381
|
+
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
382
|
+
|
|
383
|
+
// A tenant entry: "tid" (old lists) or {tid, domain, fixed, label, href}.
|
|
384
|
+
function tenantOf(x) {
|
|
385
|
+
if (typeof x === "string") return { tid: x.trim().toLowerCase(), domain: "" };
|
|
386
|
+
return Object.assign({}, x, { tid: String(x.tid || x.id || "").trim().toLowerCase(), domain: x.domain || "" });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Back from «Tenant hinzufügen»: ?entra_tenant=added|cancelled|failed|forbidden (scalio-identity 0.5.0).
|
|
390
|
+
function consentResult() {
|
|
391
|
+
if (typeof window === "undefined" || !window.location) return "";
|
|
392
|
+
try {
|
|
393
|
+
const url = new URL(window.location.href);
|
|
394
|
+
const res = url.searchParams.get("entra_tenant") || "";
|
|
395
|
+
if (res && window.history && window.history.replaceState) {
|
|
396
|
+
url.searchParams.delete("entra_tenant");
|
|
397
|
+
window.history.replaceState(window.history.state, "", url.pathname + url.search + url.hash);
|
|
398
|
+
}
|
|
399
|
+
return res;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
return "";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Props: value {source: "local"|"entra", values: [...], tenants?: [tid | {tid, domain, fixed}]},
|
|
406
|
+
// onSave({source, tenants: [{tid, domain}]}) -> Promise, tenants (show the tenant list), consentUrl (button «Tenant
|
|
407
|
+
// hinzufügen», e.g. "/api/auth/scalio/consent"), manual (default true: enter a tenant ID by hand), fixed (one fixed
|
|
408
|
+
// tenant, no add or remove: Nexus), extraTenants [{tid, domain, label, href}] and extraTitle (view only: Rosetta's
|
|
409
|
+
// customer tenants), lang, labels, confirm, notify, formatError, disabled, testId.
|
|
344
410
|
function RoleSourceSettings(props) {
|
|
345
411
|
const lang = props.lang === "en" ? "en" : "de";
|
|
346
412
|
const L = rolesText(lang, props.labels);
|
|
347
413
|
const v = props.value || { source: "local", values: [] };
|
|
414
|
+
const list = (v.tenants || []).map(tenantOf).filter((t) => t.tid);
|
|
348
415
|
const [source, setSource] = React.useState(v.source || "local");
|
|
349
|
-
const [
|
|
416
|
+
const [manual, setManual] = React.useState(null); // null = closed, else the typed id
|
|
350
417
|
const [busy, setBusy] = React.useState(false);
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
418
|
+
const [error, setError] = React.useState("");
|
|
419
|
+
const say = (text, tone) => props.notify && props.notify(text, tone || "success");
|
|
420
|
+
const fmt = props.formatError || errorText;
|
|
421
|
+
React.useEffect(() => { setSource(v.source || "local"); }, [v.source]);
|
|
422
|
+
React.useEffect(() => {
|
|
423
|
+
const res = consentResult();
|
|
424
|
+
if (res === "added") say(L.tenantAdded);
|
|
425
|
+
else if (res === "cancelled") say(L.consentCancelled, "error");
|
|
426
|
+
else if (res === "forbidden") say(L.consentForbidden, "error");
|
|
427
|
+
else if (res) say(L.consentFailed, "error");
|
|
428
|
+
}, []);
|
|
429
|
+
const dirty = source !== (v.source || "local");
|
|
430
|
+
const store = async (src, tenants, okText) => {
|
|
355
431
|
setBusy(true);
|
|
432
|
+
setError("");
|
|
356
433
|
try {
|
|
357
|
-
await props.onSave({ source, tenants: tenants.
|
|
358
|
-
|
|
434
|
+
await props.onSave({ source: src, tenants: tenants.map((t) => ({ tid: t.tid, domain: t.domain || "" })) });
|
|
435
|
+
say(okText || L.saved);
|
|
436
|
+
return true;
|
|
359
437
|
} catch (e) {
|
|
360
|
-
|
|
438
|
+
say(fmt(e), "error");
|
|
439
|
+
return false;
|
|
361
440
|
} finally {
|
|
362
441
|
setBusy(false);
|
|
363
442
|
}
|
|
364
443
|
};
|
|
444
|
+
const save = async () => {
|
|
445
|
+
if (source === "entra" && v.source !== "entra" && !(await ask(props.confirm, L.entraConfirm, L.entraConfirmText, L.entraAction, false))) return;
|
|
446
|
+
store(source, list.filter((t) => !t.fixed));
|
|
447
|
+
};
|
|
448
|
+
const remove = async (t) => {
|
|
449
|
+
if (!(await ask(props.confirm, fill(L.tenantRemoveTitle, { name: t.domain || t.tid }), L.tenantRemoveText, L.tenantRemove))) return;
|
|
450
|
+
store(v.source || "local", list.filter((x) => !x.fixed && x.tid !== t.tid), L.tenantRemoved);
|
|
451
|
+
};
|
|
452
|
+
const addManual = async (e) => {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
const tid = (manual || "").trim().toLowerCase();
|
|
455
|
+
if (!GUID.test(tid)) { setError(L.tenantInvalid); return; }
|
|
456
|
+
if (list.some((t) => t.tid === tid)) { setManual(null); return; }
|
|
457
|
+
if (await store(v.source || "local", [...list.filter((t) => !t.fixed), { tid, domain: "" }], L.tenantAdded)) setManual(null);
|
|
458
|
+
};
|
|
365
459
|
const values = (v.values || []).map((x) => (typeof x === "string" ? x : x.value || x.name)).filter(Boolean);
|
|
460
|
+
const off = props.disabled || busy;
|
|
461
|
+
const row = (t, extra) => h("li", { key: t.tid, className: "scalio-tenant-row", "data-testid": `tenant-${t.tid}` },
|
|
462
|
+
h("div", { className: "scalio-tenant-who" },
|
|
463
|
+
h("div", { className: "scalio-tenant-domain" }, t.href ? h("a", { href: t.href }, t.domain || t.label || t.tid) : (t.domain || t.label || t.tid),
|
|
464
|
+
t.label && t.domain ? h("span", { className: "scalio-badge" }, t.label) : null),
|
|
465
|
+
t.domain || t.label ? h("div", { className: "scalio-tenant-id" }, t.tid) : null),
|
|
466
|
+
extra ? null : t.fixed || props.fixed
|
|
467
|
+
? h("span", { className: "scalio-badge" }, L.tenantFixed)
|
|
468
|
+
: h("button", { type: "button", className: "scalio-btn is-ghost scalio-roles-tool is-danger", onClick: () => remove(t), disabled: off, "data-testid": `tenant-remove-${t.tid}` }, L.tenantRemove));
|
|
469
|
+
const tenantBlock = props.tenants ? h("div", { className: "scalio-users-field", "data-testid": "role-source-tenants" },
|
|
470
|
+
h("div", { className: "scalio-field-label" }, L.tenants),
|
|
471
|
+
list.length
|
|
472
|
+
? h("ul", { className: "scalio-tenant-list" }, list.map((t) => row(t, false)))
|
|
473
|
+
: h("p", { className: "scalio-users-hint", "data-testid": "tenants-empty" }, L.tenantsEmpty),
|
|
474
|
+
h("p", { className: "scalio-users-hint" }, L.tenantsHint),
|
|
475
|
+
props.fixed ? null : h("div", { className: "scalio-tenant-actions" },
|
|
476
|
+
props.consentUrl ? h("a", { className: cx("scalio-btn is-primary", off && "is-disabled"), href: props.consentUrl, title: L.tenantAddHint, "data-testid": "tenant-add" },
|
|
477
|
+
h(Icons.Plus, { className: "scalio-icon" }), L.tenantAdd) : null,
|
|
478
|
+
props.manual !== false && manual === null ? h("button", { type: "button", className: "scalio-btn", onClick: () => { setError(""); setManual(""); }, disabled: off, "data-testid": "tenant-manual" }, L.tenantManual) : null),
|
|
479
|
+
manual !== null ? h("form", { className: "scalio-tenant-manual", onSubmit: addManual },
|
|
480
|
+
h("label", { className: "scalio-field-label", htmlFor: "tenant-manual-id" }, L.tenantId),
|
|
481
|
+
h("input", { id: "tenant-manual-id", className: "scalio-input", value: manual, autoFocus: true, placeholder: "00000000-0000-0000-0000-000000000000",
|
|
482
|
+
onChange: (e) => setManual(e.target.value), disabled: off, "data-testid": "tenant-manual-id" }),
|
|
483
|
+
h("p", { className: "scalio-users-hint" }, L.tenantManualHint),
|
|
484
|
+
error ? h("p", { className: "scalio-notice is-error", role: "alert", "data-testid": "tenant-manual-error" }, error) : null,
|
|
485
|
+
h("div", { className: "scalio-tenant-actions" },
|
|
486
|
+
h("button", { type: "button", className: "scalio-btn is-ghost", onClick: () => setManual(null) }, L.cancel),
|
|
487
|
+
h("button", { type: "submit", className: "scalio-btn is-primary", disabled: off || !manual.trim(), "data-testid": "tenant-manual-save" }, L.tenantAddManual))) : null,
|
|
488
|
+
props.extraTenants && props.extraTenants.length ? h("div", { className: "scalio-users-field", "data-testid": "role-source-extra" },
|
|
489
|
+
props.extraTitle ? h("div", { className: "scalio-field-label" }, props.extraTitle) : null,
|
|
490
|
+
h("ul", { className: "scalio-tenant-list" }, props.extraTenants.map(tenantOf).map((t) => row(t, true)))) : null) : null;
|
|
366
491
|
return h("div", { className: "scalio-roles-source", "data-testid": props.testId || "role-source" },
|
|
367
492
|
h("div", { className: "scalio-users-field" },
|
|
368
493
|
h("label", { className: "scalio-field-label", htmlFor: "role-source-select" }, L.source),
|
|
369
|
-
h("
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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,
|
|
494
|
+
h("div", { className: "scalio-tenant-actions" },
|
|
495
|
+
h("select", { id: "role-source-select", className: "scalio-input scalio-users-select", value: source, disabled: off,
|
|
496
|
+
onChange: (e) => setSource(e.target.value), "data-testid": "role-source-select" },
|
|
497
|
+
h("option", { value: "local" }, L.sourceLocal),
|
|
498
|
+
h("option", { value: "entra" }, L.sourceEntra)),
|
|
499
|
+
h("button", { type: "button", className: "scalio-btn is-primary", onClick: save, disabled: off || !dirty, "data-testid": "role-source-save" }, L.save))),
|
|
378
500
|
h("p", { className: "scalio-users-hint", "data-testid": "role-source-hint" }, source === "entra" ? L.hintEntra : L.hintLocal, " ",
|
|
379
501
|
values.map((x, i) => h(React.Fragment, { key: x }, i ? ", " : "", h("code", null, x)))),
|
|
380
|
-
|
|
381
|
-
h("button", { type: "button", className: "scalio-btn is-primary", onClick: save, disabled: props.disabled || busy || !dirty, "data-testid": "role-source-save" }, L.save)));
|
|
502
|
+
tenantBlock);
|
|
382
503
|
}
|
|
383
504
|
|
|
384
505
|
module.exports = { RoleAdmin, RoleSourceSettings, rolesApi, rolesText };
|