@scalio-cloud/ui 0.33.0 → 0.34.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 CHANGED
@@ -403,6 +403,13 @@ lists `tenant` («Microsoft 365 über die Tenant-Verbindung», for apps that alr
403
403
  tenant, like Nexus), the block asks only for the sending mailbox and saves `graph: {sender}`; `tenantHint` replaces
404
404
  the default text about Mail.Send. Without `providers` the three providers stay as before.
405
405
 
406
+ Since 0.34.0 (scalio-platform 0.12.0): when the backend lists `tenants` (an app connected to several tenants, like
407
+ Citadel, Foundry and Pulse), the provider `tenant` also asks «Tenant, der sendet» and saves `graph: {sender, tenant_id}`
408
+ (an empty list shows a hint to connect a tenant first; with one tenant it is preselected). When the settings report
409
+ `send_rights`, the block «Senderecht in Exchange Online» shows the PowerShell from `GET /mail/send-rights` (Exchange
410
+ «RBAC for Applications», only the sending mailbox) with a copy button; `mailApi` has `sendRights()`. The default
411
+ `graphHint` and `tenantHint` now point to that script instead of the Entra permission Mail.Send.
412
+
406
413
  ## Protokoll: Aktionen und Jobs (React)
407
414
 
408
415
  One menu item «Protokoll» with two tabs like Nexus (scalio-docs ADR 0070). The server side is scalio-platform
package/nav.css CHANGED
@@ -754,6 +754,7 @@
754
754
  .scalio-mail-test { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
755
755
  .scalio-mail-test .scalio-input { flex: 1 1 15rem; width: auto; }
756
756
  .scalio-mail-outbox { overflow-x: auto; }
757
+ .scalio-mail-script { margin: 0 0 0.75rem; padding: 0.75rem; max-height: 22rem; overflow: auto; border: 1px solid hsl(var(--border)); border-radius: 2px; background: hsl(var(--muted)); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; line-height: 1.5; white-space: pre; }
757
758
  .scalio-mail-outbox table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
758
759
  .scalio-mail-outbox td { padding: 0.5rem 0.75rem 0.5rem 0; border-bottom: 1px solid hsl(var(--border)); vertical-align: top; }
759
760
  .scalio-mail-outbox tr:last-child td { border-bottom: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.33.0",
3
+ "version": "0.34.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/mail.js CHANGED
@@ -27,8 +27,17 @@ const LABELS = {
27
27
  clientId: "Client-ID der App",
28
28
  clientSecret: "Client-Secret",
29
29
  sender: "Postfach, das sendet",
30
- graphHint: "Die App braucht die Anwendungsberechtigung Mail.Send. Beschränken Sie sie in Exchange Online mit «RBAC for Applications» auf das Postfach, das sendet.",
31
- tenantHint: "Die App sendet mit ihrer bestehenden Verbindung zum Tenant, ohne zweites Secret. Die Verbindung braucht die Anwendungsberechtigung Mail.Send; beschränken Sie sie in Exchange Online mit «RBAC for Applications» auf das Postfach, das sendet.",
30
+ graphHint: "Das Senderecht geben Sie in Exchange Online mit «RBAC for Applications» nur für das Postfach, das sendet (Skript unten, nach dem Speichern); die App braucht dann keine Berechtigung Mail.Send in Entra ID.",
31
+ tenantHint: "Die App sendet mit ihrer bestehenden Verbindung zum Tenant, ohne zweites Secret. Das Senderecht geben Sie in Exchange Online nur für dieses Postfach (Skript unten, nach dem Speichern).",
32
+ tenantPick: "Tenant, der sendet",
33
+ tenantChoose: "Bitte wählen",
34
+ noTenants: "Noch kein Tenant verbunden. Verbinden Sie zuerst einen Tenant, dann wählen Sie ihn hier.",
35
+ rightsTitle: "Senderecht in Exchange Online",
36
+ rightsHint: "Damit die App nur als dieses Postfach senden darf: Führen Sie das Skript einmal als Exchange-Administrator in der Azure Cloud Shell (PowerShell) aus. Es nutzt «RBAC for Applications» und kann erneut laufen, auch nach einem Wechsel des Postfachs. Exchange übernimmt das Recht nach bis zu zwei Stunden; dann die Testmail senden.",
37
+ showScript: "Skript anzeigen",
38
+ copy: "Kopieren",
39
+ copied: "Skript kopiert",
40
+ copyFailed: "Kopieren nicht möglich. Bitte markieren und kopieren.",
32
41
  testMailbox: "Testinstanz: Mails gehen über das Testpostfach, die Einstellungen hier gelten nicht.",
33
42
  save: "Speichern",
34
43
  saved: "Gespeichert",
@@ -84,8 +93,17 @@ const LABELS = {
84
93
  clientId: "Client ID of the app",
85
94
  clientSecret: "Client secret",
86
95
  sender: "Mailbox that sends",
87
- graphHint: "The app needs the application permission Mail.Send. Limit it to the sending mailbox with RBAC for Applications in Exchange Online.",
88
- tenantHint: "The app sends with its existing connection to the tenant, no second secret. The connection needs the application permission Mail.Send; limit it to the sending mailbox with RBAC for Applications in Exchange Online.",
96
+ graphHint: "Give the send right in Exchange Online with RBAC for Applications, only for the sending mailbox (script below, after saving); the app then needs no Mail.Send permission in Entra ID.",
97
+ tenantHint: "The app sends with its existing connection to the tenant, no second secret. Give the send right in Exchange Online for this mailbox only (script below, after saving).",
98
+ tenantPick: "Tenant that sends",
99
+ tenantChoose: "Please choose",
100
+ noTenants: "No tenant connected yet. Connect a tenant first, then choose it here.",
101
+ rightsTitle: "Send right in Exchange Online",
102
+ rightsHint: "So the app may send only as this mailbox: run the script once as an Exchange administrator in Azure Cloud Shell (PowerShell). It uses RBAC for Applications and can run again, also after changing the mailbox. Exchange applies the right within up to two hours; then send the test mail.",
103
+ showScript: "Show script",
104
+ copy: "Copy",
105
+ copied: "Script copied",
106
+ copyFailed: "Copying is not possible. Please select and copy.",
89
107
  testMailbox: "Test instance: mails go through the test mailbox, the settings here do not apply.",
90
108
  save: "Save",
91
109
  saved: "Saved",
@@ -166,6 +184,7 @@ function mailApi(api, base) {
166
184
  resetTemplate: (kind, lang) => api.delete(`${b}/templates/${enc(kind)}/${enc(lang)}`).then(data),
167
185
  preview: (body) => api.post(`${b}/templates/preview`, body).then(data),
168
186
  testTemplate: (body) => api.post(`${b}/templates/test`, body).then(data),
187
+ sendRights: () => api.get(`${b}/send-rights`).then(data),
169
188
  };
170
189
  }
171
190
 
@@ -202,7 +221,9 @@ function select(id, value, options, onChange, extra) {
202
221
  // Props: client (mailApi), lang, labels, notify(message, tone), formatError(err), graphHint (the app's own text for
203
222
  // the Graph permissions, e.g. Beacon also needs Mail.ReadWrite), tenantHint (the same for the provider «tenant»,
204
223
  // offered when the backend lists it in settings.providers, scalio-platform 0.10.0), children (more blocks after «Testmail», e.g. Beacon's
205
- // mail intake), outbox (false hides «Letzte Mails»).
224
+ // mail intake), outbox (false hides «Letzte Mails»). With scalio-platform 0.12.0 the backend may list `tenants` (the app is
225
+ // connected to several tenants: the administrator picks the one that sends) and `send_rights` (the block «Senderecht in
226
+ // Exchange Online» shows the PowerShell from GET /mail/send-rights).
206
227
  function MailTransport(props) {
207
228
  const { client, lang } = props;
208
229
  const L = mailLabels(lang, props.labels);
@@ -213,6 +234,9 @@ function MailTransport(props) {
213
234
  const [testing, setTesting] = React.useState(false);
214
235
  const [testMsg, setTestMsg] = React.useState(null);
215
236
  const [rows, setRows] = React.useState([]);
237
+ const [script, setScript] = React.useState(null);
238
+ const [scriptBusy, setScriptBusy] = React.useState(false);
239
+ const [scriptMsg, setScriptMsg] = React.useState(null);
216
240
  const showOutbox = props.outbox !== false;
217
241
 
218
242
  const loadOutbox = React.useCallback(() => {
@@ -240,8 +264,12 @@ function MailTransport(props) {
240
264
  const body = { provider: form.provider, from_name: form.from_name, from_address: form.from_address, language: form.language };
241
265
  if (form.provider === "smtp") body.smtp = Object.assign({}, form.smtp, { port: Number(form.smtp.port) || 587 });
242
266
  if (form.provider === "graph") body.graph = form.graph;
243
- if (form.provider === "tenant") body.graph = { sender: form.graph.sender };
267
+ if (form.provider === "tenant") {
268
+ body.graph = { sender: form.graph.sender };
269
+ if (Array.isArray(form.tenants)) body.graph.tenant_id = form.graph.tenant_id || "";
270
+ }
244
271
  setForm(await client.save(body));
272
+ setScript(null);
245
273
  m.ok(L.saved);
246
274
  } catch (err) {
247
275
  m.fail(err);
@@ -265,6 +293,30 @@ function MailTransport(props) {
265
293
  setTesting(false);
266
294
  };
267
295
 
296
+ const showScript = async () => {
297
+ setScriptBusy(true);
298
+ setScriptMsg(null);
299
+ try {
300
+ const r = client.sendRights ? await client.sendRights() : null;
301
+ setScript((r && r.script) || "");
302
+ } catch (err) {
303
+ setScriptMsg({ tone: "error", text: (props.formatError || errorText)(err) });
304
+ }
305
+ setScriptBusy(false);
306
+ };
307
+ const copyScript = async () => {
308
+ let text = L.copied, tone = "success";
309
+ try {
310
+ await navigator.clipboard.writeText(script);
311
+ } catch (e) {
312
+ text = L.copyFailed;
313
+ tone = "error";
314
+ }
315
+ if (props.notify) props.notify(text, tone);
316
+ else setScriptMsg({ tone: tone === "error" ? "error" : "ok", text });
317
+ };
318
+ const tenants = Array.isArray(form.tenants) ? form.tenants : null;
319
+
268
320
  const providers = (form.providers || ["off", "smtp", "graph"]).filter((id) => L.providers[id]);
269
321
  const transport = h(
270
322
  SettingsSection,
@@ -313,7 +365,17 @@ function MailTransport(props) {
313
365
  : null,
314
366
  form.provider === "tenant"
315
367
  ? h("div", { "data-testid": "mail-tenant" },
368
+ tenants && tenants.length === 0
369
+ ? h("div", { className: "scalio-notice is-info", "data-testid": "mail-no-tenants" }, L.noTenants)
370
+ : null,
316
371
  h("div", { className: "scalio-mail-grid" },
372
+ tenants && tenants.length > 0
373
+ ? h(Field, { id: "mail-tenant-pick", label: L.tenantPick },
374
+ select("mail-tenant-pick",
375
+ tenants.some((t) => t.id === form.graph.tenant_id) ? form.graph.tenant_id : (tenants.length === 1 ? tenants[0].id : ""),
376
+ (tenants.length === 1 ? [] : [["", L.tenantChoose]]).concat(tenants.map((t) => [t.id, t.name || t.id])),
377
+ part("graph", "tenant_id"), { disabled: busy }))
378
+ : null,
317
379
  h(Field, { id: "mail-tenant-sender", label: L.sender }, input("mail-tenant-sender", form.graph.sender, part("graph", "sender"),
318
380
  { type: "email", maxLength: 254, placeholder: "it@example.com", disabled: busy }))),
319
381
  h("p", { className: "scalio-settings-help" }, props.tenantHint || L.tenantHint))
@@ -335,6 +397,23 @@ function MailTransport(props) {
335
397
  h(Notice, { msg: testMsg, testId: "mail-test-message" })
336
398
  );
337
399
 
400
+ const rights = form.send_rights && (form.provider === "graph" || form.provider === "tenant")
401
+ ? h(
402
+ SettingsSection,
403
+ { id: "mail-rights", title: L.rightsTitle, description: L.rightsHint, testId: "mail-rights" },
404
+ script
405
+ ? h(React.Fragment, null,
406
+ h("pre", { className: "scalio-mail-script", "data-testid": "mail-rights-script" }, script),
407
+ h("div", { className: "scalio-settings-section-footer" },
408
+ h("button", { type: "button", className: "scalio-btn", onClick: copyScript, "data-testid": "mail-rights-copy" },
409
+ h(Icons.Copy, { className: "scalio-icon" }), L.copy)))
410
+ : h("div", { className: "scalio-settings-section-footer" },
411
+ h("button", { type: "button", className: "scalio-btn", onClick: showScript, disabled: scriptBusy, "data-testid": "mail-rights-show" },
412
+ L.showScript)),
413
+ h(Notice, { msg: scriptMsg, testId: "mail-rights-message" })
414
+ )
415
+ : null;
416
+
338
417
  const outbox = showOutbox
339
418
  ? h(
340
419
  SettingsSection,
@@ -355,7 +434,7 @@ function MailTransport(props) {
355
434
  )
356
435
  : null;
357
436
 
358
- return h(React.Fragment, null, transport, testBlock, props.children || null, outbox);
437
+ return h(React.Fragment, null, transport, rights, testBlock, props.children || null, outbox);
359
438
  }
360
439
 
361
440
  // Props: client (mailApi), lang, labels, notify, formatError, editor (the app's rich text editor: a component with