@scalio-cloud/ui 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -1
- package/nav.css +8 -0
- package/package.json +1 -1
- package/react/labels.js +12 -0
- package/react/license.js +40 -15
- package/react/support.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
- Self-hosted support (ADR 0022, T11; needs scalio-connect 0.7.0 in the backend): `supportApi` gets `status()` and
|
|
5
|
+
`setButtonHidden(hidden)`. The support drawer asks `status()` first; a self-hosted installation without a
|
|
6
|
+
connection shows no form, only a note and the button "Zum Scalio-Portal" (`<portal>/support`). The card
|
|
7
|
+
"Support-Verbindung" gets the switch "Support-Knopf in der Kopfzeile ausblenden". **Visible change** only where
|
|
8
|
+
the backend reports `link_only` or the client has `setButtonHidden`; no token changes.
|
|
9
|
+
|
|
10
|
+
## 0.10.0
|
|
11
|
+
- `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
|
|
12
|
+
`badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
|
|
13
|
+
`addonsHidden`, `addonsReadOnly` and `keyReadOnly` (a note instead of the form) and a `copied` text per row.
|
|
14
|
+
Lets Nexus keep its trials, demo mode and badges on the shared panel. **No visible change** for apps that use
|
|
15
|
+
`mapScalioLicense`.
|
|
16
|
+
|
|
3
17
|
## 0.9.0
|
|
4
18
|
- New `CommandPalette` (⌘K / Strg+K): finds every page of the sidebar data (`nav`) plus extra `pages`, remembers the
|
|
5
19
|
last visited pages, and takes app commands as `groups` (Nexus: AI and guarded actions). Arrow keys, Enter, Esc,
|
package/README.md
CHANGED
|
@@ -214,7 +214,8 @@ const license = licenseApi(api);
|
|
|
214
214
|
`securityApi` reads snake_case and camelCase answers. `notify(text, tone)` gets `"success"` or `"error"`; without it
|
|
215
215
|
the blocks show the message themselves. `confirm` is the app's dialog (`{title, action, danger}` -> Promise<boolean>),
|
|
216
216
|
`window.confirm` otherwise. `formatError(err)` lets the app phrase API errors (for example license limit codes). A license backend with other routes passes its own `client` and a `map(license, {L})`
|
|
217
|
-
that returns the view model described in `react/license.js`.
|
|
217
|
+
that returns the view model described in `react/license.js`. Nexus does this: its `map` adds trial badges, seat hints, invalid
|
|
218
|
+
add-ons and read-only notes (demo mode), and passes its trial notices as `statusExtra` and the reminder settings as `children`.
|
|
218
219
|
|
|
219
220
|
## Command palette (React)
|
|
220
221
|
|
package/nav.css
CHANGED
|
@@ -400,3 +400,11 @@
|
|
|
400
400
|
.scalio-palette { max-height: 85vh; }
|
|
401
401
|
.scalio-palette-foot, .scalio-palette-enter { display: none; }
|
|
402
402
|
}
|
|
403
|
+
.scalio-license-bar-hint { margin: 0.25rem 0 0; font-size: 0.6875rem; color: hsl(var(--primary)); }
|
|
404
|
+
.scalio-license-addon-name { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; }
|
|
405
|
+
.scalio-license-addon-name .scalio-badge { font-size: 10px; }
|
|
406
|
+
.scalio-license-addon.tone-info { border-color: hsl(var(--primary) / 0.35); background: hsl(var(--primary) / 0.05); }
|
|
407
|
+
.scalio-license-addon.tone-warn { border-color: #fcd34d; background: rgb(254 243 199 / 0.5); }
|
|
408
|
+
.dark .scalio-license-addon.tone-warn { border-color: rgb(251 191 36 / 0.4); background: rgb(251 191 36 / 0.08); }
|
|
409
|
+
.scalio-license-addon-error { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
410
|
+
.scalio-license-link { padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
@@ -88,6 +88,12 @@ const LABELS = {
|
|
|
88
88
|
supportConnStatus: { unpaired: "Nicht verbunden", pending: "Wartet auf Bestätigung", active: "Verbunden", blocked: "Gesperrt" },
|
|
89
89
|
supportConnHelp: "Tickets aus dieser Installation gehen an den Scalio Support. Den einmaligen Code erhalten Sie im Scalio-Portal unter Support -> Verbundene Apps.",
|
|
90
90
|
supportConnHelpConnected: "Tickets aus dieser Installation gehen an den Scalio Support.",
|
|
91
|
+
supportLinkOnly: "Diese Installation ist nicht mit dem Scalio Support verbunden. Eröffnen Sie Ihre Anfrage bitte im Scalio-Portal.",
|
|
92
|
+
supportLinkOnlyAdmin: "Ein Admin kann die Verbindung unter Einstellungen -> Support-Verbindung einrichten, dann gehen Anfragen direkt aus der App.",
|
|
93
|
+
supportOpenPortal: "Zum Scalio-Portal",
|
|
94
|
+
supportButtonHidden: "Support-Knopf in der Kopfzeile ausblenden",
|
|
95
|
+
supportButtonHiddenNote: "Der Support-Knopf ist ausgeblendet.",
|
|
96
|
+
supportButtonShownNote: "Der Support-Knopf ist wieder sichtbar.",
|
|
91
97
|
supportConnPending: "Ein Administrator Ihrer Organisation muss die Verbindung im Scalio-Portal bestätigen.",
|
|
92
98
|
supportConnBlocked: "Diese Installation ist für den Support gesperrt. Bitte wenden Sie sich an Scalio.",
|
|
93
99
|
supportConnKeyLost: "Der Schlüssel dieser Verbindung kann nicht mehr gelesen werden. Bitte verbinden Sie neu.",
|
|
@@ -277,6 +283,12 @@ const LABELS = {
|
|
|
277
283
|
supportConnStatus: { unpaired: "Not connected", pending: "Waiting for confirmation", active: "Connected", blocked: "Blocked" },
|
|
278
284
|
supportConnHelp: "Tickets from this installation go to Scalio Support. You get the one-time code in the Scalio portal under Support -> Connected apps.",
|
|
279
285
|
supportConnHelpConnected: "Tickets from this installation go to Scalio Support.",
|
|
286
|
+
supportLinkOnly: "This installation is not connected to Scalio Support. Please open your request in the Scalio portal.",
|
|
287
|
+
supportLinkOnlyAdmin: "An admin can set up the connection under Settings -> Support connection; then requests go straight from the app.",
|
|
288
|
+
supportOpenPortal: "Open the Scalio portal",
|
|
289
|
+
supportButtonHidden: "Hide the support button in the header",
|
|
290
|
+
supportButtonHiddenNote: "The support button is hidden.",
|
|
291
|
+
supportButtonShownNote: "The support button is visible again.",
|
|
280
292
|
supportConnPending: "An administrator of your organisation must confirm the connection in the Scalio portal.",
|
|
281
293
|
supportConnBlocked: "This installation is blocked for support. Please contact Scalio.",
|
|
282
294
|
supportConnKeyLost: "The key of this connection can no longer be read. Please connect again.",
|
package/react/license.js
CHANGED
|
@@ -36,8 +36,11 @@ function licenseApi(api) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// View model:
|
|
39
|
-
// { edition, licensed, customer, managed, messages[], rows[{label, value, tone, mono, copy, testId}],
|
|
40
|
-
// features[{key, label}] | null, usage[{key, label, used, max
|
|
39
|
+
// { edition, licensed, customer, managed, messages[], rows[{label, value, tone, mono, copy, copied, testId}],
|
|
40
|
+
// features[{key, label}] | null, usage[{key, label, used, max, hint}],
|
|
41
|
+
// addons[{id, name, seats, exp, detail, badges[{label, tone}], tone, removable}], addonErrors[{key, error, addon}], keyPresent,
|
|
42
|
+
// addonsHidden, addonsReadOnly (note instead of the add-on form), keyReadOnly (note instead of the key form) }
|
|
43
|
+
// Optional fields let an app with its own license model (Nexus: trials, demo) show more without its own panel.
|
|
41
44
|
function mapScalioLicense(lic, opts) {
|
|
42
45
|
const o = opts || {};
|
|
43
46
|
const L = o.L || labelsFor(o.lang).license;
|
|
@@ -94,7 +97,8 @@ function UsageBar({ item }) {
|
|
|
94
97
|
h("span", { className: "scalio-license-bar-label" }, item.label),
|
|
95
98
|
h("span", { className: cx("scalio-license-bar-value", full && "is-error") }, used === null ? "-" : used, " / ", unlimited ? "∞" : item.max)
|
|
96
99
|
),
|
|
97
|
-
h("div", { className: "scalio-license-bar" }, h("div", { className: cx(full && "is-full", unlimited && "is-unlimited"), style: { width: `${pct}%` } }))
|
|
100
|
+
h("div", { className: "scalio-license-bar" }, h("div", { className: cx(full && "is-full", unlimited && "is-unlimited"), style: { width: `${pct}%` } })),
|
|
101
|
+
item.hint ? h("p", { className: "scalio-license-bar-hint", "data-testid": `license-limit-${item.key}-hint` }, item.hint) : null
|
|
98
102
|
);
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -155,10 +159,10 @@ function LicensePanel(props) {
|
|
|
155
159
|
const addAddon = async () => {
|
|
156
160
|
if (await run(() => client.addAddon(addon.trim()), L.addonActivated)) setAddon("");
|
|
157
161
|
};
|
|
158
|
-
const copy = async (value) => {
|
|
162
|
+
const copy = async (value, copied) => {
|
|
159
163
|
try {
|
|
160
164
|
await navigator.clipboard.writeText(value);
|
|
161
|
-
say(L.idCopied);
|
|
165
|
+
say(copied || L.idCopied);
|
|
162
166
|
} catch (e) {
|
|
163
167
|
say(L.copyFailed, "error");
|
|
164
168
|
}
|
|
@@ -170,7 +174,7 @@ function LicensePanel(props) {
|
|
|
170
174
|
{ key: r.label + i },
|
|
171
175
|
h("dt", null, r.label),
|
|
172
176
|
r.copy
|
|
173
|
-
? h("dd", null, h("button", { type: "button", className: "scalio-license-copy", title: L.copy, onClick: () => copy(r.value), "data-testid": r.testId },
|
|
177
|
+
? h("dd", null, h("button", { type: "button", className: "scalio-license-copy", title: L.copy, onClick: () => copy(r.value, r.copied), "data-testid": r.testId },
|
|
174
178
|
r.value, h(Icons.Copy, { className: "scalio-icon", width: 12, height: 12 })))
|
|
175
179
|
: h("dd", { className: cx(r.mono && "is-mono"), "data-testid": r.testId }, r.tone ? h("span", { className: `is-${r.tone}` }, r.value) : r.value)
|
|
176
180
|
);
|
|
@@ -219,7 +223,7 @@ function LicensePanel(props) {
|
|
|
219
223
|
? h("div", { className: "scalio-license-usage" }, h("div", { className: "scalio-license-sub" }, L.usage), m.usage.map((u) => h(UsageBar, { key: u.key, item: u })))
|
|
220
224
|
: null
|
|
221
225
|
),
|
|
222
|
-
h(
|
|
226
|
+
m.addonsHidden ? null : h(
|
|
223
227
|
"div",
|
|
224
228
|
{ className: "scalio-license-part", "data-testid": "license-addons" },
|
|
225
229
|
h("div", { className: "scalio-license-sub" }, h(Icons.Puzzle, { className: "scalio-icon", width: 14, height: 14 }), L.addons),
|
|
@@ -230,16 +234,23 @@ function LicensePanel(props) {
|
|
|
230
234
|
m.addons.map((a) =>
|
|
231
235
|
h(
|
|
232
236
|
"div",
|
|
233
|
-
{ key: a.id, className: "scalio-license-addon", "data-testid": `addon-card-${a.id}` },
|
|
237
|
+
{ key: a.id, className: cx("scalio-license-addon", a.tone && `tone-${a.tone}`), "data-testid": `addon-card-${a.id}` },
|
|
234
238
|
h(
|
|
235
239
|
"div",
|
|
236
240
|
{ style: { minWidth: 0 } },
|
|
237
|
-
h(
|
|
241
|
+
h(
|
|
242
|
+
"div",
|
|
243
|
+
{ className: "scalio-license-addon-name" },
|
|
244
|
+
a.name,
|
|
245
|
+
(a.badges || []).map((b) =>
|
|
246
|
+
h("span", { key: b.label, className: cx("scalio-badge", b.tone && `is-${b.tone}`), "data-testid": b.testId }, b.label))
|
|
247
|
+
),
|
|
238
248
|
h("p", { className: "scalio-license-addon-detail" },
|
|
239
|
-
a.
|
|
240
|
-
|
|
249
|
+
a.detail !== undefined
|
|
250
|
+
? a.detail
|
|
251
|
+
: [a.seats ? `+${a.seats}` : h("span", { key: "i", className: "is-ok" }, L.included), a.exp ? ` · ${L.maintenanceUntil} ${a.exp}` : ""])
|
|
241
252
|
),
|
|
242
|
-
m.managed
|
|
253
|
+
m.managed || m.addonsReadOnly || a.removable === false
|
|
243
254
|
? null
|
|
244
255
|
: h("button", { type: "button", className: "scalio-btn is-ghost is-danger", disabled: busy, title: L.remove, "aria-label": L.remove,
|
|
245
256
|
"data-testid": `addon-remove-${a.id}`, onClick: () => run(() => client.removeAddon(a), L.addonRemoved) },
|
|
@@ -248,8 +259,20 @@ function LicensePanel(props) {
|
|
|
248
259
|
)
|
|
249
260
|
)
|
|
250
261
|
: h("p", { className: "scalio-license-empty" }, L.noAddons),
|
|
251
|
-
m.addonErrors.map((e) =>
|
|
252
|
-
|
|
262
|
+
m.addonErrors.map((e) =>
|
|
263
|
+
h(
|
|
264
|
+
"div",
|
|
265
|
+
{ key: e.key, className: "scalio-license-addon-error", "data-testid": e.testId },
|
|
266
|
+
h("span", null, e.error),
|
|
267
|
+
e.addon && !m.managed && !m.addonsReadOnly
|
|
268
|
+
? h("button", { type: "button", className: "scalio-license-link", disabled: busy, "data-testid": `addon-remove-${e.addon.id}`,
|
|
269
|
+
onClick: () => run(() => client.removeAddon(e.addon), L.addonRemoved) }, L.remove)
|
|
270
|
+
: null
|
|
271
|
+
)
|
|
272
|
+
),
|
|
273
|
+
m.addonsReadOnly
|
|
274
|
+
? h("div", { className: "scalio-notice is-info", "data-testid": "license-addon-readonly" }, m.addonsReadOnly)
|
|
275
|
+
: m.managed
|
|
253
276
|
? null
|
|
254
277
|
: h(
|
|
255
278
|
"div",
|
|
@@ -261,7 +284,9 @@ function LicensePanel(props) {
|
|
|
261
284
|
h(Icons.Plus, { className: "scalio-icon" }), L.add))
|
|
262
285
|
)
|
|
263
286
|
),
|
|
264
|
-
m.managed
|
|
287
|
+
m.keyReadOnly && !m.managed
|
|
288
|
+
? h("div", { className: "scalio-license-part" }, h("div", { className: "scalio-notice is-info", "data-testid": "license-key-readonly" }, m.keyReadOnly))
|
|
289
|
+
: m.managed
|
|
265
290
|
? null
|
|
266
291
|
: h(
|
|
267
292
|
"div",
|
package/react/support.js
CHANGED
|
@@ -29,6 +29,8 @@ function supportApi(api) {
|
|
|
29
29
|
connect: (portal_url, code) => data(api.post("/support/connection", { portal_url, code })),
|
|
30
30
|
check: () => data(api.post("/support/connection/check")),
|
|
31
31
|
disconnect: () => data(api.delete("/support/connection")),
|
|
32
|
+
status: () => data(api.get("/support/status")),
|
|
33
|
+
setButtonHidden: (hidden) => data(api.put("/support/connection/button", { hidden })),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -223,8 +225,27 @@ function TicketDetail({ client, id, L, lang, onBack }) {
|
|
|
223
225
|
);
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
// Self-hosted without a connection (ADR 0022, T11): no form, only the way to the portal's support page.
|
|
229
|
+
function PortalLink({ status, L }) {
|
|
230
|
+
return h(
|
|
231
|
+
"div",
|
|
232
|
+
{ className: "scalio-support-form", "data-testid": "support-link-only" },
|
|
233
|
+
h("p", { className: "scalio-support-meta" }, L.supportLinkOnly),
|
|
234
|
+
h("a", { className: "scalio-btn-primary", href: status.portal_page, target: "_blank", rel: "noopener noreferrer",
|
|
235
|
+
"data-testid": "support-portal-link" }, L.supportOpenPortal),
|
|
236
|
+
h("p", { className: "scalio-support-meta" }, L.supportLinkOnlyAdmin)
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
function SupportDesk({ client, page, lang, labels }) {
|
|
227
241
|
const L = labelsFor(lang, labels);
|
|
242
|
+
const [status, setStatus] = React.useState(client.status ? null : {});
|
|
243
|
+
React.useEffect(() => {
|
|
244
|
+
if (!client.status) return undefined;
|
|
245
|
+
let alive = true;
|
|
246
|
+
client.status().then((d) => alive && setStatus(d || {})).catch(() => alive && setStatus({}));
|
|
247
|
+
return () => { alive = false; };
|
|
248
|
+
}, [client]);
|
|
228
249
|
const [tab, setTab] = React.useState("new");
|
|
229
250
|
const [ticket, setTicket] = React.useState(null);
|
|
230
251
|
const [note, setNote] = React.useState("");
|
|
@@ -233,6 +254,8 @@ function SupportDesk({ client, page, lang, labels }) {
|
|
|
233
254
|
type: "button", role: "tab", "aria-selected": tab === key, className: cx("scalio-support-tab", tab === key && "is-active"),
|
|
234
255
|
onClick: () => { setTab(key); setTicket(null); setNote(""); }, "data-testid": `support-tab-${key}`,
|
|
235
256
|
}, label);
|
|
257
|
+
if (status === null) return h("div", { className: "scalio-support-meta" }, L.loading);
|
|
258
|
+
if (status.link_only && status.portal_page) return h(PortalLink, { status, L });
|
|
236
259
|
return h(
|
|
237
260
|
React.Fragment,
|
|
238
261
|
null,
|
|
@@ -313,6 +336,15 @@ function SupportConnection({ client, lang, labels, onChange }) {
|
|
|
313
336
|
: null,
|
|
314
337
|
note ? h("div", { className: "scalio-notice is-ok", "data-testid": "support-note" }, note) : null,
|
|
315
338
|
error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "support-error" }, error) : null,
|
|
339
|
+
client.setButtonHidden
|
|
340
|
+
? h(
|
|
341
|
+
"label",
|
|
342
|
+
{ className: "scalio-support-check", "data-testid": "support-button-hidden" },
|
|
343
|
+
h("input", { type: "checkbox", checked: !!st.button_hidden, disabled: busy,
|
|
344
|
+
onChange: (e) => { const hide = e.target.checked; run(() => client.setButtonHidden(hide), hide ? L.supportButtonHiddenNote : L.supportButtonShownNote); } }),
|
|
345
|
+
h("span", null, L.supportButtonHidden)
|
|
346
|
+
)
|
|
347
|
+
: null,
|
|
316
348
|
connected
|
|
317
349
|
? h(
|
|
318
350
|
"div",
|