@scalio-cloud/ui 0.10.0 → 0.12.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 +13 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/react/icons.js +2 -1
- package/react/index.js +43 -1
- package/react/labels.js +14 -0
- package/react/support.js +32 -0
- package/shell.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
- New manual button in `AppHeader` (prop `docsUrl`): a book icon between theme and support that opens the product
|
|
5
|
+
manual in a new tab; `DocsButton` on its own, `manualUrl(product, { stage, lang })` builds the address
|
|
6
|
+
(`devdocs.<domain>` on dev and test, `docs.<domain>` on production, demo and self-hosted, German under `/de/`).
|
|
7
|
+
**Visible change** only in apps that pass `docsUrl`; no token changes.
|
|
8
|
+
|
|
9
|
+
## 0.11.0
|
|
10
|
+
- Self-hosted support (ADR 0022, T11; needs scalio-connect 0.7.0 in the backend): `supportApi` gets `status()` and
|
|
11
|
+
`setButtonHidden(hidden)`. The support drawer asks `status()` first; a self-hosted installation without a
|
|
12
|
+
connection shows no form, only a note and the button "Zum Scalio-Portal" (`<portal>/support`). The card
|
|
13
|
+
"Support-Verbindung" gets the switch "Support-Knopf in der Kopfzeile ausblenden". **Visible change** only where
|
|
14
|
+
the backend reports `link_only` or the client has `setButtonHidden`; no token changes.
|
|
15
|
+
|
|
3
16
|
## 0.10.0
|
|
4
17
|
- `LicensePanel` takes more from an app's own `map`: a `hint` per usage bar (e.g. base + add-on seats), add-ons with
|
|
5
18
|
`badges`, `detail`, `tone` (`info`/`warn`) and `removable: false`, `addonErrors` (invalid add-ons, removable),
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ support, avatar.
|
|
|
45
45
|
A part only appears when the app passes its handler (Compass has no dark mode, so no `onThemeChange`).
|
|
46
46
|
|
|
47
47
|
```jsx
|
|
48
|
-
import { AppHeader, PageHeader, VersionTag } from "@scalio-cloud/ui/react";
|
|
48
|
+
import { AppHeader, PageHeader, VersionTag, manualUrl } from "@scalio-cloud/ui/react";
|
|
49
49
|
|
|
50
50
|
<AppHeader
|
|
51
51
|
brand={{ mark: "N", name: "Scalio Nexus", sub: "Dev" }} // or your own element
|
|
@@ -55,6 +55,7 @@ import { AppHeader, PageHeader, VersionTag } from "@scalio-cloud/ui/react";
|
|
|
55
55
|
status={<ApiStatus />} // hidden below 768px
|
|
56
56
|
lang={lang} onLangChange={setLang} // one click, shows the other language
|
|
57
57
|
theme={theme} themeModes={["light", "dark", "system"]} onThemeChange={setTheme}
|
|
58
|
+
docsUrl={manualUrl("nexus", { stage, lang })} // book icon, opens the manual in a new tab
|
|
58
59
|
onSupport={() => setSupportOpen(true)}
|
|
59
60
|
user={{ name, email, role }} onLogout={logout}
|
|
60
61
|
menuItems={[{ label: "Profil", icon: User, onSelect: () => nav("/profile"), testId: "profile-link" }]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalio-cloud/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
|
|
5
5
|
"license": "SEE LICENSE IN README.md",
|
|
6
6
|
"private": false,
|
package/react/icons.js
CHANGED
|
@@ -32,6 +32,7 @@ const LifeBuoy = icon([
|
|
|
32
32
|
["circle", { cx: 12, cy: 12, r: 4 }],
|
|
33
33
|
["path", { d: "m4.93 4.93 4.24 4.24M14.83 9.17l4.24-4.24M14.83 14.83l4.24 4.24M9.17 14.83l-4.24 4.24" }],
|
|
34
34
|
]);
|
|
35
|
+
const BookOpen = icon([["path", { d: "M12 7v14" }], ["path", { d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" }]]);
|
|
35
36
|
const LogOut = icon([["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" }]]);
|
|
36
37
|
const Menu = icon([["path", { d: "M4 6h16M4 12h16M4 18h16" }]]);
|
|
37
38
|
const Check = icon([["path", { d: "M20 6 9 17l-5-5" }]]);
|
|
@@ -99,5 +100,5 @@ const History = icon([
|
|
|
99
100
|
]);
|
|
100
101
|
const CornerDownLeft = icon([["path", { d: "M20 4v7a4 4 0 0 1-4 4H4" }], ["path", { d: "m9 10-5 5 5 5" }]]);
|
|
101
102
|
|
|
102
|
-
module.exports = { Sun, Moon, Monitor, LifeBuoy, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
|
|
103
|
+
module.exports = { Sun, Moon, Monitor, LifeBuoy, BookOpen, LogOut, Menu, Check, X, Search, ChevronDown, Fingerprint, FileStack, ShieldCheck, Hammer, Activity,
|
|
103
104
|
Smartphone, KeyRound, Trash, Copy, Puzzle, Plus, Alert, History, CornerDownLeft };
|
package/react/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const TEST_IDS = {
|
|
|
10
10
|
menu: "mobile-menu-btn",
|
|
11
11
|
language: "language-toggle-btn",
|
|
12
12
|
theme: "theme-toggle-btn",
|
|
13
|
+
docs: "docs-link-btn",
|
|
13
14
|
support: "support-ticket-trigger",
|
|
14
15
|
avatar: "user-avatar-btn",
|
|
15
16
|
userName: "user-name",
|
|
@@ -233,6 +234,43 @@ function SupportButton({ onClick, lang, labels, testId }) {
|
|
|
233
234
|
);
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
// The customer manual of a product: devdocs.<domain> on our dev and test instances, docs.<domain> everywhere
|
|
238
|
+
// else (production, demo, self-hosted). German readers get the German version under /de/.
|
|
239
|
+
const DOCS_DOMAINS = {
|
|
240
|
+
nexus: "scalionexus.com",
|
|
241
|
+
citadel: "scaliocitadel.com",
|
|
242
|
+
foundry: "scaliofoundry.com",
|
|
243
|
+
rosetta: "scaliorosetta.com",
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function manualUrl(product, opts) {
|
|
247
|
+
const o = opts || {};
|
|
248
|
+
const domain = DOCS_DOMAINS[product] || product;
|
|
249
|
+
if (!domain || domain.indexOf(".") < 0) return null;
|
|
250
|
+
const stage = String(o.stage || "").toLowerCase();
|
|
251
|
+
const host = (stage === "dev" || stage === "test" ? "devdocs." : "docs.") + domain;
|
|
252
|
+
return `https://${host}/${o.lang === "de" ? "de/" : ""}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Opens the manual in a new tab.
|
|
256
|
+
function DocsButton({ href, lang, labels, testId }) {
|
|
257
|
+
if (!href) return null;
|
|
258
|
+
const L = labelsFor(lang, labels);
|
|
259
|
+
return h(
|
|
260
|
+
"a",
|
|
261
|
+
{
|
|
262
|
+
className: "scalio-btn scalio-btn-icon",
|
|
263
|
+
href,
|
|
264
|
+
target: "_blank",
|
|
265
|
+
rel: "noopener noreferrer",
|
|
266
|
+
title: L.docs,
|
|
267
|
+
"aria-label": L.docs,
|
|
268
|
+
"data-testid": testId || TEST_IDS.docs,
|
|
269
|
+
},
|
|
270
|
+
h(Icons.BookOpen, { className: "scalio-icon" })
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
236
274
|
function Brand({ brand }) {
|
|
237
275
|
if (!brand) return null;
|
|
238
276
|
if (React.isValidElement(brand)) return brand;
|
|
@@ -249,7 +287,7 @@ function Brand({ brand }) {
|
|
|
249
287
|
return h("div", { className: "scalio-brand" }, inner);
|
|
250
288
|
}
|
|
251
289
|
|
|
252
|
-
// Header: [menu] brand, left slot ... app actions, status, language, theme, support, avatar.
|
|
290
|
+
// Header: [menu] brand, left slot ... app actions, status, language, theme, manual, support, avatar.
|
|
253
291
|
// The part from status to avatar has the same order in every app.
|
|
254
292
|
function AppHeader(props) {
|
|
255
293
|
const L = labelsFor(props.lang, props.labels);
|
|
@@ -284,6 +322,7 @@ function AppHeader(props) {
|
|
|
284
322
|
labels: props.labels,
|
|
285
323
|
testId: T.theme,
|
|
286
324
|
}),
|
|
325
|
+
h(DocsButton, { href: props.docsUrl, lang: props.lang, labels: props.labels, testId: T.docs }),
|
|
287
326
|
h(SupportButton, { onClick: props.onSupport, lang: props.lang, labels: props.labels, testId: T.support }),
|
|
288
327
|
props.user
|
|
289
328
|
? h(UserMenu, {
|
|
@@ -326,6 +365,9 @@ module.exports = {
|
|
|
326
365
|
LanguageToggle,
|
|
327
366
|
ThemeToggle,
|
|
328
367
|
SupportButton,
|
|
368
|
+
DocsButton,
|
|
369
|
+
manualUrl,
|
|
370
|
+
DOCS_DOMAINS,
|
|
329
371
|
PageHeader,
|
|
330
372
|
VersionTag,
|
|
331
373
|
Icons,
|
package/react/labels.js
CHANGED
|
@@ -8,6 +8,7 @@ const LABELS = {
|
|
|
8
8
|
dark: "Dunkel",
|
|
9
9
|
system: "System",
|
|
10
10
|
support: "Support",
|
|
11
|
+
docs: "Handbuch (öffnet in neuem Tab)",
|
|
11
12
|
logout: "Abmelden",
|
|
12
13
|
userMenu: "Benutzermenü",
|
|
13
14
|
menu: "Menü",
|
|
@@ -88,6 +89,12 @@ const LABELS = {
|
|
|
88
89
|
supportConnStatus: { unpaired: "Nicht verbunden", pending: "Wartet auf Bestätigung", active: "Verbunden", blocked: "Gesperrt" },
|
|
89
90
|
supportConnHelp: "Tickets aus dieser Installation gehen an den Scalio Support. Den einmaligen Code erhalten Sie im Scalio-Portal unter Support -> Verbundene Apps.",
|
|
90
91
|
supportConnHelpConnected: "Tickets aus dieser Installation gehen an den Scalio Support.",
|
|
92
|
+
supportLinkOnly: "Diese Installation ist nicht mit dem Scalio Support verbunden. Eröffnen Sie Ihre Anfrage bitte im Scalio-Portal.",
|
|
93
|
+
supportLinkOnlyAdmin: "Ein Admin kann die Verbindung unter Einstellungen -> Support-Verbindung einrichten, dann gehen Anfragen direkt aus der App.",
|
|
94
|
+
supportOpenPortal: "Zum Scalio-Portal",
|
|
95
|
+
supportButtonHidden: "Support-Knopf in der Kopfzeile ausblenden",
|
|
96
|
+
supportButtonHiddenNote: "Der Support-Knopf ist ausgeblendet.",
|
|
97
|
+
supportButtonShownNote: "Der Support-Knopf ist wieder sichtbar.",
|
|
91
98
|
supportConnPending: "Ein Administrator Ihrer Organisation muss die Verbindung im Scalio-Portal bestätigen.",
|
|
92
99
|
supportConnBlocked: "Diese Installation ist für den Support gesperrt. Bitte wenden Sie sich an Scalio.",
|
|
93
100
|
supportConnKeyLost: "Der Schlüssel dieser Verbindung kann nicht mehr gelesen werden. Bitte verbinden Sie neu.",
|
|
@@ -202,6 +209,7 @@ const LABELS = {
|
|
|
202
209
|
dark: "Dark",
|
|
203
210
|
system: "System",
|
|
204
211
|
support: "Support",
|
|
212
|
+
docs: "Manual (opens in a new tab)",
|
|
205
213
|
logout: "Sign out",
|
|
206
214
|
userMenu: "User menu",
|
|
207
215
|
menu: "Menu",
|
|
@@ -277,6 +285,12 @@ const LABELS = {
|
|
|
277
285
|
supportConnStatus: { unpaired: "Not connected", pending: "Waiting for confirmation", active: "Connected", blocked: "Blocked" },
|
|
278
286
|
supportConnHelp: "Tickets from this installation go to Scalio Support. You get the one-time code in the Scalio portal under Support -> Connected apps.",
|
|
279
287
|
supportConnHelpConnected: "Tickets from this installation go to Scalio Support.",
|
|
288
|
+
supportLinkOnly: "This installation is not connected to Scalio Support. Please open your request in the Scalio portal.",
|
|
289
|
+
supportLinkOnlyAdmin: "An admin can set up the connection under Settings -> Support connection; then requests go straight from the app.",
|
|
290
|
+
supportOpenPortal: "Open the Scalio portal",
|
|
291
|
+
supportButtonHidden: "Hide the support button in the header",
|
|
292
|
+
supportButtonHiddenNote: "The support button is hidden.",
|
|
293
|
+
supportButtonShownNote: "The support button is visible again.",
|
|
280
294
|
supportConnPending: "An administrator of your organisation must confirm the connection in the Scalio portal.",
|
|
281
295
|
supportConnBlocked: "This installation is blocked for support. Please contact Scalio.",
|
|
282
296
|
supportConnKeyLost: "The key of this connection can no longer be read. Please connect again.",
|
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",
|
package/shell.css
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
}
|
|
46
46
|
@media (min-width: 640px) { .scalio-btn { padding: 0 1rem; } }
|
|
47
47
|
.scalio-btn:hover { background: hsl(var(--secondary)); }
|
|
48
|
+
a.scalio-btn { text-decoration: none; }
|
|
48
49
|
.scalio-btn:focus-visible, .scalio-avatar:focus-visible, .scalio-menu-item:focus-visible {
|
|
49
50
|
outline: 2px solid hsl(var(--ring)); outline-offset: 1px;
|
|
50
51
|
}
|