@cosmicdrift/kumiko-renderer-web 0.258.1 → 0.259.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.259.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.259.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.259.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.259.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/react-dom": "^19.2.3",
|
|
67
67
|
"jsdom": "^29.1.1",
|
|
68
68
|
"tailwindcss": "^4.3.0",
|
|
69
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
69
|
+
"@cosmicdrift/kumiko-locale-de": "0.259.0"
|
|
70
70
|
},
|
|
71
71
|
"repository": {
|
|
72
72
|
"type": "git",
|
|
@@ -3,11 +3,13 @@ import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui
|
|
|
3
3
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import type { ExtensionSectionProps, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import {
|
|
6
|
+
AppFeaturesProvider,
|
|
6
7
|
DashboardBodyProvider,
|
|
7
8
|
DispatcherProvider,
|
|
8
9
|
ExtensionSectionsProvider,
|
|
9
10
|
KumikoScreen,
|
|
10
11
|
NavProvider,
|
|
12
|
+
UserRolesProvider,
|
|
11
13
|
} from "@cosmicdrift/kumiko-renderer";
|
|
12
14
|
import userEvent from "@testing-library/user-event";
|
|
13
15
|
import type { ReactNode } from "react";
|
|
@@ -527,3 +529,151 @@ describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
|
|
|
527
529
|
expect(kpiCall?.payload).toEqual({ tenantId: "t-2" });
|
|
528
530
|
});
|
|
529
531
|
});
|
|
532
|
+
|
|
533
|
+
const sessionsSchema: FeatureSchema = {
|
|
534
|
+
featureName: "sessions",
|
|
535
|
+
entities: {},
|
|
536
|
+
screens: [
|
|
537
|
+
{
|
|
538
|
+
id: "my-sessions",
|
|
539
|
+
type: "projectionList",
|
|
540
|
+
query: "sessions:query:user-session:mine",
|
|
541
|
+
columns: [{ field: "userAgent", label: "sessions.mine.col.userAgent" }],
|
|
542
|
+
access: { openToAll: true },
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "session-list",
|
|
546
|
+
type: "projectionList",
|
|
547
|
+
query: "sessions:query:user-session:list",
|
|
548
|
+
columns: [{ field: "userAgent", label: "sessions.list.col.userAgent" }],
|
|
549
|
+
access: { roles: ["SystemAdmin"] },
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const mfaSchema: FeatureSchema = {
|
|
555
|
+
featureName: "auth-mfa",
|
|
556
|
+
entities: {},
|
|
557
|
+
screens: [
|
|
558
|
+
{
|
|
559
|
+
id: "auth-mfa-enable",
|
|
560
|
+
type: "secretMint",
|
|
561
|
+
handler: "auth-mfa:write:enable-start",
|
|
562
|
+
fields: {},
|
|
563
|
+
layout: { sections: [] },
|
|
564
|
+
submitLabel: "mfa.enable.start",
|
|
565
|
+
reveal: { fields: [{ field: "recoveryCodes", label: "codes", display: "list" }] },
|
|
566
|
+
access: { openToAll: true },
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "auth-mfa-disable",
|
|
570
|
+
type: "actionForm",
|
|
571
|
+
handler: "auth-mfa:write:disable",
|
|
572
|
+
fields: { code: { type: "text", required: true } },
|
|
573
|
+
layout: { sections: [{ fields: ["code"] }] },
|
|
574
|
+
submitLabel: "mfa.disable.submit",
|
|
575
|
+
access: { openToAll: true },
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const mfaStatus = { query: "auth-mfa:query:user-mfa:status", field: "enabled" } as const;
|
|
581
|
+
|
|
582
|
+
const accountSecuritySchema: FeatureSchema = {
|
|
583
|
+
featureName: "account-security",
|
|
584
|
+
entities: {},
|
|
585
|
+
screens: [
|
|
586
|
+
{
|
|
587
|
+
id: "account-security",
|
|
588
|
+
type: "dashboard",
|
|
589
|
+
panels: [
|
|
590
|
+
{
|
|
591
|
+
kind: "screen",
|
|
592
|
+
id: "mfa-enable",
|
|
593
|
+
screen: "auth-mfa:screen:auth-mfa-enable",
|
|
594
|
+
visibleWhen: { ...mfaStatus, eq: false },
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
kind: "screen",
|
|
598
|
+
id: "mfa-disable",
|
|
599
|
+
screen: "auth-mfa:screen:auth-mfa-disable",
|
|
600
|
+
visibleWhen: { ...mfaStatus, eq: true },
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
kind: "screen",
|
|
604
|
+
id: "sessions",
|
|
605
|
+
screen: "sessions:screen:my-sessions",
|
|
606
|
+
label: "account-security:panel:sessions",
|
|
607
|
+
},
|
|
608
|
+
{ kind: "screen", id: "admin-sessions", screen: "sessions:screen:session-list" },
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
function renderAccountSecurity(mfaEnabled: boolean): void {
|
|
615
|
+
const dispatcher = createMockDispatcher({
|
|
616
|
+
query: (async (type: string) => {
|
|
617
|
+
if (type === mfaStatus.query) return { isSuccess: true, data: { enabled: mfaEnabled } };
|
|
618
|
+
if (type === "sessions:query:user-session:mine") {
|
|
619
|
+
return {
|
|
620
|
+
isSuccess: true,
|
|
621
|
+
data: { rows: [{ id: "s1", userAgent: "Firefox on Linux" }], nextCursor: null },
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
isSuccess: true,
|
|
626
|
+
data: { rows: [{ id: "x", userAgent: "ADMIN ROW" }], nextCursor: null },
|
|
627
|
+
};
|
|
628
|
+
}) as unknown as Dispatcher["query"],
|
|
629
|
+
});
|
|
630
|
+
render(
|
|
631
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
632
|
+
<AppFeaturesProvider features={[accountSecuritySchema, sessionsSchema, mfaSchema]}>
|
|
633
|
+
<UserRolesProvider roles={["User"]}>
|
|
634
|
+
<BrowserNav>
|
|
635
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
636
|
+
<KumikoScreen
|
|
637
|
+
schema={accountSecuritySchema}
|
|
638
|
+
qn="account-security:screen:account-security"
|
|
639
|
+
/>
|
|
640
|
+
</DashboardBodyProvider>
|
|
641
|
+
</BrowserNav>
|
|
642
|
+
</UserRolesProvider>
|
|
643
|
+
</AppFeaturesProvider>
|
|
644
|
+
</DispatcherProvider>,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
describe("KumikoScreen dashboard — screen-Panels (fw#2841)", () => {
|
|
649
|
+
afterEach(() => {
|
|
650
|
+
window.history.replaceState(null, "", "/");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("bettet einen Cross-Feature-projectionList-Screen samt Label ein", async () => {
|
|
654
|
+
renderAccountSecurity(false);
|
|
655
|
+
await waitFor(() => expect(screen.getByText("Firefox on Linux")).toBeTruthy());
|
|
656
|
+
expect(screen.getByTestId("dashboard-panel-sessions")).toBeTruthy();
|
|
657
|
+
expect(screen.getByText("account-security:panel:sessions")).toBeTruthy();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("visibleWhen zeigt nur das Panel, das zum Query-Wert passt", async () => {
|
|
661
|
+
renderAccountSecurity(false);
|
|
662
|
+
await waitFor(() => expect(screen.getByTestId("dashboard-panel-mfa-enable")).toBeTruthy());
|
|
663
|
+
expect(screen.queryByTestId("dashboard-panel-mfa-disable")).toBeNull();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("visibleWhen mit umgekehrtem Status blendet das jeweils andere Panel ein", async () => {
|
|
667
|
+
renderAccountSecurity(true);
|
|
668
|
+
await waitFor(() => expect(screen.getByTestId("dashboard-panel-mfa-disable")).toBeTruthy());
|
|
669
|
+
expect(screen.queryByTestId("dashboard-panel-mfa-enable")).toBeNull();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("ohne Zugriff auf den Ziel-Screen fällt die Kachel komplett weg (kein Access-Banner)", async () => {
|
|
673
|
+
renderAccountSecurity(false);
|
|
674
|
+
await waitFor(() => expect(screen.getByText("Firefox on Linux")).toBeTruthy());
|
|
675
|
+
expect(screen.queryByTestId("dashboard-panel-admin-sessions")).toBeNull();
|
|
676
|
+
expect(screen.queryByText("ADMIN ROW")).toBeNull();
|
|
677
|
+
expect(screen.queryByTestId("kumiko-screen-access-denied")).toBeNull();
|
|
678
|
+
});
|
|
679
|
+
});
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
// progress-list → { rows: { id, label, value, fraction }[] }
|
|
24
24
|
// custom → keine Query — eine über extensionSectionComponents
|
|
25
25
|
// registrierte App-Komponente holt sich ihre Daten selbst.
|
|
26
|
+
// screen → no own query; embeds another declarative screen via
|
|
27
|
+
// KumikoScreen. visibleWhen reads a flat record (live); the
|
|
28
|
+
// tile is dropped when the user can't access the target.
|
|
26
29
|
//
|
|
27
30
|
// Screen-Filter (DashboardFilterDefinition): der gewählte Wert wird unter
|
|
28
31
|
// `filter.id` in JEDE Panel-Query gemerged. useQuery refetcht automatisch
|
|
@@ -36,13 +39,17 @@ import type {
|
|
|
36
39
|
DashboardPanelDefinition,
|
|
37
40
|
DashboardProgressListPanel,
|
|
38
41
|
DashboardScreenDefinition,
|
|
42
|
+
DashboardScreenPanel,
|
|
39
43
|
DashboardStatGroupPanel,
|
|
40
44
|
DashboardStatPanel,
|
|
41
45
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
42
46
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
47
|
+
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
43
48
|
import {
|
|
44
49
|
type DashboardBodyProps,
|
|
45
50
|
extensionSectionName,
|
|
51
|
+
KumikoScreen,
|
|
52
|
+
useEmbeddedScreen,
|
|
46
53
|
useExtensionSectionComponent,
|
|
47
54
|
useNav,
|
|
48
55
|
usePrimitives,
|
|
@@ -403,7 +410,7 @@ function PanelBody({
|
|
|
403
410
|
screenId,
|
|
404
411
|
filterParams,
|
|
405
412
|
}: {
|
|
406
|
-
readonly panel: DashboardPanelDefinition
|
|
413
|
+
readonly panel: Exclude<DashboardPanelDefinition, DashboardScreenPanel>;
|
|
407
414
|
readonly label: string;
|
|
408
415
|
readonly screenId: string;
|
|
409
416
|
readonly filterParams: Readonly<Record<string, unknown>>;
|
|
@@ -434,7 +441,43 @@ function PanelBody({
|
|
|
434
441
|
return <CustomPanelBody panel={panel} screenId={screenId} filterParams={filterParams} />;
|
|
435
442
|
}
|
|
436
443
|
|
|
437
|
-
|
|
444
|
+
function ScreenPanelTile({
|
|
445
|
+
panel,
|
|
446
|
+
featureName,
|
|
447
|
+
translate,
|
|
448
|
+
}: {
|
|
449
|
+
readonly panel: DashboardScreenPanel;
|
|
450
|
+
readonly featureName: string;
|
|
451
|
+
readonly translate: Translate;
|
|
452
|
+
}): ReactNode {
|
|
453
|
+
const visibleWhen = panel.visibleWhen;
|
|
454
|
+
const visibility = useQuery<Readonly<Record<string, unknown>>>(
|
|
455
|
+
visibleWhen?.query ?? "",
|
|
456
|
+
{},
|
|
457
|
+
{ enabled: visibleWhen !== undefined, live: true },
|
|
458
|
+
);
|
|
459
|
+
const target = useEmbeddedScreen(featureName, panel.screen);
|
|
460
|
+
if (target === undefined) return null;
|
|
461
|
+
if (visibleWhen !== undefined && visibility.data?.[visibleWhen.field] !== visibleWhen.eq) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const embedded = <KumikoScreen schema={target.schema} qn={target.qn} translate={translate} />;
|
|
465
|
+
return (
|
|
466
|
+
<div className={WIDE_PANEL} data-testid={`dashboard-panel-${panel.id}`}>
|
|
467
|
+
{panel.label !== undefined ? (
|
|
468
|
+
<SectionCard title={translate(panel.label)}>{embedded}</SectionCard>
|
|
469
|
+
) : (
|
|
470
|
+
embedded
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function WebDashboardBody({
|
|
477
|
+
featureName,
|
|
478
|
+
screen,
|
|
479
|
+
translate,
|
|
480
|
+
}: DashboardBodyProps): ReactNode {
|
|
438
481
|
const t = useTranslation();
|
|
439
482
|
const effectiveTranslate = translate ?? t;
|
|
440
483
|
const { Text } = usePrimitives();
|
|
@@ -449,6 +492,16 @@ export function WebDashboardBody({ screen, translate }: DashboardBodyProps): Rea
|
|
|
449
492
|
{picker}
|
|
450
493
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
451
494
|
{screen.panels.map((panel) => {
|
|
495
|
+
if (panel.kind === "screen") {
|
|
496
|
+
return (
|
|
497
|
+
<ScreenPanelTile
|
|
498
|
+
key={panel.id}
|
|
499
|
+
panel={panel}
|
|
500
|
+
featureName={featureName}
|
|
501
|
+
translate={effectiveTranslate}
|
|
502
|
+
/>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
452
505
|
const label = panel.kind === "custom" ? "" : effectiveTranslate(panel.label);
|
|
453
506
|
const span = panelSpanClassName(panel);
|
|
454
507
|
return (
|