@cosmicdrift/kumiko-bundled-features 0.239.0 → 0.241.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 +11 -11
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
- package/src/admin-shell/changes.json +6 -0
- package/src/admin-shell/feature.ts +60 -6
- package/src/admin-shell/i18n.ts +0 -7
- package/src/admin-shell/web/client-plugin.tsx +1 -11
- package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
- package/src/audit/changes.json +7 -0
- package/src/audit/feature.ts +57 -7
- package/src/audit/handlers/list.query.ts +50 -14
- package/src/audit/i18n.ts +3 -18
- package/src/audit/web/client-plugin.tsx +1 -7
- package/src/audit/web/index.ts +0 -2
- package/src/auth-mfa/web/mfa-enable-screen.tsx +1 -5
- package/src/config/__tests__/config.integration.test.ts +31 -0
- package/src/config/handlers/readiness.query.ts +8 -1
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
- package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
- package/src/jobs/changes.json +7 -0
- package/src/jobs/feature.ts +101 -9
- package/src/jobs/handlers/list.query.ts +21 -4
- package/src/jobs/handlers/trigger.write.ts +16 -3
- package/src/jobs/i18n.ts +3 -25
- package/src/jobs/index.ts +1 -0
- package/src/jobs/web/client-plugin.tsx +1 -7
- package/src/jobs/web/index.ts +0 -2
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +1 -1
- package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
- package/src/tenant/handlers/list.query.ts +1 -0
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
- package/src/tier-engine/changes.json +9 -1
- package/src/tier-engine/constants.ts +5 -5
- package/src/tier-engine/feature.ts +36 -10
- package/src/tier-engine/i18n.ts +19 -22
- package/src/tier-engine/web/client-plugin.tsx +5 -11
- package/src/tier-engine/web/index.ts +0 -1
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
- package/src/user-data-rights/changes.json +7 -0
- package/src/user-data-rights/constants.ts +20 -6
- package/src/user-data-rights/feature.ts +113 -14
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
- package/src/user-data-rights/web/client-plugin.tsx +19 -21
- package/src/user-data-rights/web/i18n.ts +6 -11
- package/src/user-data-rights/web/index.ts +1 -1
- package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
- package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
- package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
- package/src/user-profile/changes.json +7 -0
- package/src/user-profile/constants.ts +15 -7
- package/src/user-profile/feature.ts +96 -9
- package/src/user-profile/i18n.ts +8 -12
- package/src/user-profile/web/client-plugin.ts +22 -5
- package/src/user-profile/web/index.ts +1 -1
- package/src/user-profile/web/profile-screen.tsx +122 -265
- package/src/admin-shell/web/overview-layout.tsx +0 -69
- package/src/admin-shell/web/overview-query.ts +0 -15
- package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
- package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
- package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
- package/src/audit/web/audit-log-detail-screen.tsx +0 -173
- package/src/audit/web/audit-log-screen.tsx +0 -290
- package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
- package/src/jobs/web/job-run-detail-screen.tsx +0 -181
- package/src/jobs/web/job-runs-screen.tsx +0 -290
- package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
|
|
3
|
-
import { useDispatcher, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
-
import { type ReactNode, useEffect, useState } from "react";
|
|
5
|
-
import { JobQueries } from "../../jobs/constants";
|
|
6
|
-
import { TenantQueries } from "../../tenant/constants";
|
|
7
|
-
import { UserQueries } from "../../user/constants";
|
|
8
|
-
import { OverviewLayout, type OverviewState } from "./overview-layout";
|
|
9
|
-
import { overviewQuery } from "./overview-query";
|
|
10
|
-
|
|
11
|
-
export function PlatformOverviewScreen(): ReactNode {
|
|
12
|
-
const t = useTranslation();
|
|
13
|
-
const dispatcher = useDispatcher();
|
|
14
|
-
const [state, setState] = useState<OverviewState>({ kind: "loading" });
|
|
15
|
-
|
|
16
|
-
// kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
void (async (): Promise<void> => {
|
|
20
|
-
const tenantsRes = await overviewQuery<{ readonly total: number }>(
|
|
21
|
-
"platform",
|
|
22
|
-
dispatcher,
|
|
23
|
-
TenantQueries.list,
|
|
24
|
-
{ totalCount: true },
|
|
25
|
-
);
|
|
26
|
-
if (cancelled) return;
|
|
27
|
-
if (!tenantsRes.isSuccess) {
|
|
28
|
-
setState({ kind: "error", message: tenantsRes.error.message });
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const usersRes = await overviewQuery<{ readonly total: number }>(
|
|
33
|
-
"platform",
|
|
34
|
-
dispatcher,
|
|
35
|
-
UserQueries.list,
|
|
36
|
-
{ totalCount: true },
|
|
37
|
-
);
|
|
38
|
-
if (cancelled) return;
|
|
39
|
-
if (!usersRes.isSuccess) {
|
|
40
|
-
setState({ kind: "error", message: usersRes.error.message });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const failedJobsRes = await overviewQuery<{ readonly total: number }>(
|
|
45
|
-
"platform",
|
|
46
|
-
dispatcher,
|
|
47
|
-
JobQueries.list,
|
|
48
|
-
{ status: "failed", totalCount: true },
|
|
49
|
-
);
|
|
50
|
-
if (cancelled) return;
|
|
51
|
-
if (!failedJobsRes.isSuccess) {
|
|
52
|
-
setState({ kind: "error", message: failedJobsRes.error.message });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
setState({
|
|
57
|
-
kind: "ready",
|
|
58
|
-
cards: [
|
|
59
|
-
{
|
|
60
|
-
label: t("admin-shell:overview.tenants"),
|
|
61
|
-
value: String(tenantsRes.data.total),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
label: t("admin-shell:overview.users"),
|
|
65
|
-
value: String(usersRes.data.total),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
label: t("admin-shell:overview.failedJobs"),
|
|
69
|
-
value: String(failedJobsRes.data.total),
|
|
70
|
-
hint:
|
|
71
|
-
failedJobsRes.data.total > 0 ? t("admin-shell:overview.failedJobsHint") : undefined,
|
|
72
|
-
attention: failedJobsRes.data.total > 0,
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
});
|
|
76
|
-
})();
|
|
77
|
-
return () => {
|
|
78
|
-
cancelled = true;
|
|
79
|
-
};
|
|
80
|
-
}, [dispatcher, t]);
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<OverviewLayout
|
|
84
|
-
testId="platform-overview-screen"
|
|
85
|
-
title={t("admin-shell:overview.platformTitle")}
|
|
86
|
-
state={state}
|
|
87
|
-
loadingLabel={t("admin-shell:overview.loading")}
|
|
88
|
-
columns={3}
|
|
89
|
-
/>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
|
|
3
|
-
import { useDispatcher, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
-
import { type ReactNode, useEffect, useState } from "react";
|
|
5
|
-
import { ConfigQueries } from "../../config/constants";
|
|
6
|
-
import { TenantQueries } from "../../tenant/constants";
|
|
7
|
-
import { OverviewLayout, type OverviewState } from "./overview-layout";
|
|
8
|
-
import { overviewQuery } from "./overview-query";
|
|
9
|
-
|
|
10
|
-
export function TenantOverviewScreen(): ReactNode {
|
|
11
|
-
const t = useTranslation();
|
|
12
|
-
const dispatcher = useDispatcher();
|
|
13
|
-
const [state, setState] = useState<OverviewState>({ kind: "loading" });
|
|
14
|
-
|
|
15
|
-
// kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
let cancelled = false;
|
|
18
|
-
void (async (): Promise<void> => {
|
|
19
|
-
const invitationsRes = await overviewQuery<readonly unknown[]>(
|
|
20
|
-
"tenant",
|
|
21
|
-
dispatcher,
|
|
22
|
-
TenantQueries.invitations,
|
|
23
|
-
{},
|
|
24
|
-
);
|
|
25
|
-
if (cancelled) return;
|
|
26
|
-
if (!invitationsRes.isSuccess) {
|
|
27
|
-
setState({ kind: "error", message: invitationsRes.error.message });
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const membersRes = await overviewQuery<readonly unknown[]>(
|
|
32
|
-
"tenant",
|
|
33
|
-
dispatcher,
|
|
34
|
-
TenantQueries.members,
|
|
35
|
-
{},
|
|
36
|
-
);
|
|
37
|
-
if (cancelled) return;
|
|
38
|
-
if (!membersRes.isSuccess) {
|
|
39
|
-
setState({ kind: "error", message: membersRes.error.message });
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const readinessRes = await overviewQuery<{ readonly missing: readonly unknown[] }>(
|
|
44
|
-
"tenant",
|
|
45
|
-
dispatcher,
|
|
46
|
-
ConfigQueries.readiness,
|
|
47
|
-
{},
|
|
48
|
-
);
|
|
49
|
-
if (cancelled) return;
|
|
50
|
-
if (!readinessRes.isSuccess) {
|
|
51
|
-
setState({ kind: "error", message: readinessRes.error.message });
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
setState({
|
|
56
|
-
kind: "ready",
|
|
57
|
-
cards: [
|
|
58
|
-
{
|
|
59
|
-
label: t("admin-shell:overview.pendingInvitations"),
|
|
60
|
-
value: String(invitationsRes.data.length),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: t("admin-shell:overview.members"),
|
|
64
|
-
value: String(membersRes.data.length),
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
label: t("admin-shell:overview.missingConfig"),
|
|
68
|
-
value: String(readinessRes.data.missing.length),
|
|
69
|
-
hint:
|
|
70
|
-
readinessRes.data.missing.length > 0
|
|
71
|
-
? t("admin-shell:overview.missingConfigHint")
|
|
72
|
-
: undefined,
|
|
73
|
-
attention: readinessRes.data.missing.length > 0,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
})();
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true;
|
|
80
|
-
};
|
|
81
|
-
}, [dispatcher, t]);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<OverviewLayout
|
|
85
|
-
testId="tenant-overview-screen"
|
|
86
|
-
title={t("admin-shell:overview.tenantTitle")}
|
|
87
|
-
state={state}
|
|
88
|
-
loadingLabel={t("admin-shell:overview.loading")}
|
|
89
|
-
/>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// DOM test for AuditLogScreen actor-name resolution (#audit-log-actor-name).
|
|
3
|
-
// Provider wrapper is local — renderer-web → bundled-features forbids importing
|
|
4
|
-
// renderer-web test-utils from here.
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from "bun:test";
|
|
7
|
-
import { createStore, type Dispatcher, type DispatcherStatus } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import {
|
|
9
|
-
createStaticLocaleResolver,
|
|
10
|
-
DispatcherProvider,
|
|
11
|
-
kumikoDefaultTranslations,
|
|
12
|
-
type LiveEventSubscriber,
|
|
13
|
-
LiveEventsProvider,
|
|
14
|
-
LocaleProvider,
|
|
15
|
-
type NavApi,
|
|
16
|
-
NavProvider,
|
|
17
|
-
PrimitivesProvider,
|
|
18
|
-
TokensProvider,
|
|
19
|
-
} from "@cosmicdrift/kumiko-renderer";
|
|
20
|
-
import { defaultPrimitives, defaultTokens } from "@cosmicdrift/kumiko-renderer-web";
|
|
21
|
-
import { render, waitFor } from "@testing-library/react";
|
|
22
|
-
import type { ReactNode } from "react";
|
|
23
|
-
import { TenantQueries } from "../../tenant/constants";
|
|
24
|
-
import { AUDIT_LOG_DETAIL_SCREEN_ID, AuditQueries } from "../constants";
|
|
25
|
-
import { AuditLogDetailScreen } from "../web/audit-log-detail-screen";
|
|
26
|
-
import { AuditLogScreen } from "../web/audit-log-screen";
|
|
27
|
-
import { defaultTranslations } from "../web/i18n";
|
|
28
|
-
|
|
29
|
-
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
30
|
-
globalThis.ResizeObserver = class {
|
|
31
|
-
observe(): void {}
|
|
32
|
-
unobserve(): void {}
|
|
33
|
-
disconnect(): void {}
|
|
34
|
-
} as unknown as typeof ResizeObserver;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const stubLiveEvents: LiveEventSubscriber = () => () => {};
|
|
38
|
-
const stubTokens = {
|
|
39
|
-
tokens: defaultTokens,
|
|
40
|
-
mode: "light" as const,
|
|
41
|
-
setMode: () => {},
|
|
42
|
-
toggleMode: () => {},
|
|
43
|
-
};
|
|
44
|
-
const stubResolver = createStaticLocaleResolver();
|
|
45
|
-
const stubNav: NavApi = {
|
|
46
|
-
route: undefined,
|
|
47
|
-
navigate: () => {},
|
|
48
|
-
replace: () => {},
|
|
49
|
-
hrefFor: (target) => {
|
|
50
|
-
if (!("screenId" in target)) return "";
|
|
51
|
-
return target.entityId !== undefined
|
|
52
|
-
? `/${target.screenId}/${target.entityId}`
|
|
53
|
-
: `/${target.screenId}`;
|
|
54
|
-
},
|
|
55
|
-
searchParams: {},
|
|
56
|
-
setSearchParams: () => {},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
type AuditRowFixture = {
|
|
60
|
-
readonly id: string;
|
|
61
|
-
readonly aggregateId: string;
|
|
62
|
-
readonly aggregateType: string;
|
|
63
|
-
readonly type: string;
|
|
64
|
-
readonly createdBy: string;
|
|
65
|
-
readonly createdAt: string;
|
|
66
|
-
readonly payload: Record<string, unknown>;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type MemberRowFixture = {
|
|
70
|
-
readonly userId: string;
|
|
71
|
-
readonly email: string | null;
|
|
72
|
-
readonly displayName: string | null;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const AUDIT_ROWS: readonly AuditRowFixture[] = [
|
|
76
|
-
{
|
|
77
|
-
id: "1",
|
|
78
|
-
aggregateId: "agg-1",
|
|
79
|
-
aggregateType: "widget",
|
|
80
|
-
type: "widget.created",
|
|
81
|
-
createdBy: "user-1",
|
|
82
|
-
createdAt: "2026-08-01T00:00:00.000Z",
|
|
83
|
-
payload: {},
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
id: "2",
|
|
87
|
-
aggregateId: "agg-2",
|
|
88
|
-
aggregateType: "widget",
|
|
89
|
-
type: "widget.updated",
|
|
90
|
-
createdBy: "user-2",
|
|
91
|
-
createdAt: "2026-08-02T00:00:00.000Z",
|
|
92
|
-
payload: {},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "3",
|
|
96
|
-
aggregateId: "agg-3",
|
|
97
|
-
aggregateType: "widget",
|
|
98
|
-
type: "widget.deleted",
|
|
99
|
-
createdBy: "user-missing",
|
|
100
|
-
createdAt: "2026-08-03T00:00:00.000Z",
|
|
101
|
-
payload: {},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "4",
|
|
105
|
-
aggregateId: "agg-4",
|
|
106
|
-
aggregateType: "widget",
|
|
107
|
-
type: "widget.synced",
|
|
108
|
-
createdBy: "system",
|
|
109
|
-
createdAt: "2026-08-04T00:00:00.000Z",
|
|
110
|
-
payload: {},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: "5",
|
|
114
|
-
aggregateId: "agg-5",
|
|
115
|
-
aggregateType: "widget",
|
|
116
|
-
type: "widget.archived",
|
|
117
|
-
createdBy: "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
|
|
118
|
-
createdAt: "2026-08-05T00:00:00.000Z",
|
|
119
|
-
payload: {},
|
|
120
|
-
},
|
|
121
|
-
];
|
|
122
|
-
|
|
123
|
-
const MEMBERS: readonly MemberRowFixture[] = [
|
|
124
|
-
{ userId: "user-1", email: "alice@example.com", displayName: "Alice Example" },
|
|
125
|
-
{ userId: "user-2", email: "bob@example.com", displayName: null },
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
type AuditDetailFixture = AuditRowFixture & { readonly metadata: Record<string, unknown> };
|
|
129
|
-
|
|
130
|
-
function detailFixtureFor(row: AuditRowFixture): AuditDetailFixture {
|
|
131
|
-
return { ...row, metadata: {} };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
type MembersResult =
|
|
135
|
-
| { readonly isSuccess: true; readonly data: readonly MemberRowFixture[] }
|
|
136
|
-
| { readonly isSuccess: false; readonly error: { readonly message: string } };
|
|
137
|
-
|
|
138
|
-
function makeDispatcher(
|
|
139
|
-
options: {
|
|
140
|
-
readonly detail?: AuditDetailFixture | null;
|
|
141
|
-
readonly membersResult?: MembersResult;
|
|
142
|
-
} = {},
|
|
143
|
-
): Dispatcher {
|
|
144
|
-
const statusStore = createStore<DispatcherStatus>("online");
|
|
145
|
-
const query = (async (type: string) => {
|
|
146
|
-
if (type === AuditQueries.list) {
|
|
147
|
-
return { isSuccess: true, data: { rows: AUDIT_ROWS, nextBefore: null } };
|
|
148
|
-
}
|
|
149
|
-
if (type === AuditQueries.details) {
|
|
150
|
-
return { isSuccess: true, data: options.detail ?? null };
|
|
151
|
-
}
|
|
152
|
-
if (type === TenantQueries.members) {
|
|
153
|
-
return options.membersResult ?? { isSuccess: true, data: MEMBERS };
|
|
154
|
-
}
|
|
155
|
-
return { isSuccess: true, data: null };
|
|
156
|
-
}) as unknown as Dispatcher["query"];
|
|
157
|
-
return {
|
|
158
|
-
write: (async () => ({ isSuccess: true, data: null })) as unknown as Dispatcher["write"],
|
|
159
|
-
query,
|
|
160
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
161
|
-
statusStore,
|
|
162
|
-
async *stream() {},
|
|
163
|
-
pendingWrites: () => [],
|
|
164
|
-
pendingFiles: () => [],
|
|
165
|
-
} as unknown as Dispatcher; // @cast-boundary test-stub
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function renderWithProviders(
|
|
169
|
-
ui: ReactNode,
|
|
170
|
-
options: { readonly nav?: NavApi; readonly dispatcher?: Dispatcher } = {},
|
|
171
|
-
): ReturnType<typeof render> {
|
|
172
|
-
const nav = options.nav ?? stubNav;
|
|
173
|
-
const dispatcher = options.dispatcher ?? makeDispatcher();
|
|
174
|
-
const wrapper = ({ children }: { readonly children: ReactNode }): ReactNode => (
|
|
175
|
-
<TokensProvider value={stubTokens}>
|
|
176
|
-
<LocaleProvider
|
|
177
|
-
resolver={stubResolver}
|
|
178
|
-
fallbackBundles={[defaultTranslations, kumikoDefaultTranslations]}
|
|
179
|
-
>
|
|
180
|
-
<PrimitivesProvider value={defaultPrimitives}>
|
|
181
|
-
<NavProvider value={nav}>
|
|
182
|
-
<LiveEventsProvider value={stubLiveEvents}>
|
|
183
|
-
<DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
|
|
184
|
-
</LiveEventsProvider>
|
|
185
|
-
</NavProvider>
|
|
186
|
-
</PrimitivesProvider>
|
|
187
|
-
</LocaleProvider>
|
|
188
|
-
</TokensProvider>
|
|
189
|
-
);
|
|
190
|
-
return render(ui, { wrapper });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function renderScreen(dispatcher?: Dispatcher): ReturnType<typeof render> {
|
|
194
|
-
return renderWithProviders(<AuditLogScreen />, { dispatcher });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function renderDetailScreen(entityId: string, dispatcher: Dispatcher): ReturnType<typeof render> {
|
|
198
|
-
const nav: NavApi = { ...stubNav, route: { screenId: AUDIT_LOG_DETAIL_SCREEN_ID, entityId } };
|
|
199
|
-
return renderWithProviders(<AuditLogDetailScreen />, { nav, dispatcher });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
describe("AuditLogScreen — actor name resolution", () => {
|
|
203
|
-
test("known member with displayName → cell shows the display name", async () => {
|
|
204
|
-
const view = renderScreen();
|
|
205
|
-
await waitFor(() => {
|
|
206
|
-
const cell = view.queryByTestId("cell-1-actor");
|
|
207
|
-
if (cell === null) throw new Error("actor cell not rendered yet");
|
|
208
|
-
});
|
|
209
|
-
expect(view.getByTestId("cell-1-actor").textContent).toBe("Alice Example");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("known member without displayName → cell falls back to email", async () => {
|
|
213
|
-
const view = renderScreen();
|
|
214
|
-
await waitFor(() => {
|
|
215
|
-
const cell = view.queryByTestId("cell-2-actor");
|
|
216
|
-
if (cell === null) throw new Error("actor cell not rendered yet");
|
|
217
|
-
});
|
|
218
|
-
expect(view.getByTestId("cell-2-actor").textContent).toBe("bob@example.com");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("createdBy not in members list → cell shows Unknown actor, never the raw id", async () => {
|
|
222
|
-
const view = renderScreen();
|
|
223
|
-
await waitFor(() => {
|
|
224
|
-
const cell = view.queryByTestId("cell-3-actor");
|
|
225
|
-
if (cell === null) throw new Error("actor cell not rendered yet");
|
|
226
|
-
});
|
|
227
|
-
const cell = view.getByTestId("cell-3-actor");
|
|
228
|
-
expect(cell.textContent).toBe("Unknown actor");
|
|
229
|
-
expect(cell.textContent).not.toBe("user-missing");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("table has no aggregate column", async () => {
|
|
233
|
-
const view = renderScreen();
|
|
234
|
-
await waitFor(() => {
|
|
235
|
-
const cell = view.queryByTestId("cell-1-actor");
|
|
236
|
-
if (cell === null) throw new Error("table not rendered yet");
|
|
237
|
-
});
|
|
238
|
-
expect(view.queryByTestId("cell-1-aggregate")).toBeNull();
|
|
239
|
-
expect(view.container.textContent).not.toContain("agg-1");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("createdBy is the system literal → cell shows the System label", async () => {
|
|
243
|
-
const view = renderScreen();
|
|
244
|
-
await waitFor(() => {
|
|
245
|
-
const cell = view.queryByTestId("cell-4-actor");
|
|
246
|
-
if (cell === null) throw new Error("actor cell not rendered yet");
|
|
247
|
-
});
|
|
248
|
-
expect(view.getByTestId("cell-4-actor").textContent).toBe("System");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("createdBy is an unresolvable UUID → Unknown actor label, raw UUID never rendered", async () => {
|
|
252
|
-
const view = renderScreen();
|
|
253
|
-
await waitFor(() => {
|
|
254
|
-
const cell = view.queryByTestId("cell-5-actor");
|
|
255
|
-
if (cell === null) throw new Error("actor cell not rendered yet");
|
|
256
|
-
});
|
|
257
|
-
const cell = view.getByTestId("cell-5-actor");
|
|
258
|
-
expect(cell.textContent).toBe("Unknown actor");
|
|
259
|
-
expect(view.container.textContent).not.toContain("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
describe("AuditLogScreen — members query failure", () => {
|
|
264
|
-
test("members query returns isSuccess:false → list still renders, no crash", async () => {
|
|
265
|
-
const dispatcher = makeDispatcher({
|
|
266
|
-
membersResult: { isSuccess: false, error: { message: "members unavailable" } },
|
|
267
|
-
});
|
|
268
|
-
const view = renderScreen(dispatcher);
|
|
269
|
-
await waitFor(() => {
|
|
270
|
-
const cell = view.queryByTestId("cell-1-type");
|
|
271
|
-
if (cell === null) throw new Error("table not rendered yet");
|
|
272
|
-
});
|
|
273
|
-
expect(view.getByTestId("cell-1-type").textContent).toBe("widget.created");
|
|
274
|
-
expect(view.getByTestId("cell-1-actor").textContent).toBe("Unknown actor");
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
describe("AuditLogDetailScreen — actor name resolution", () => {
|
|
279
|
-
test("known member with displayName → detail actor field shows the display name", async () => {
|
|
280
|
-
const row = AUDIT_ROWS[0];
|
|
281
|
-
if (row === undefined) throw new Error("fixture missing");
|
|
282
|
-
const dispatcher = makeDispatcher({ detail: detailFixtureFor(row) });
|
|
283
|
-
const view = renderDetailScreen(row.id, dispatcher);
|
|
284
|
-
await waitFor(() => {
|
|
285
|
-
const cell = view.queryByTestId("audit-detail-actor");
|
|
286
|
-
if (cell === null) throw new Error("actor field not rendered yet");
|
|
287
|
-
});
|
|
288
|
-
expect(view.getByTestId("audit-detail-actor").textContent).toBe("Alice Example");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("known member without displayName → detail actor field falls back to email", async () => {
|
|
292
|
-
const row = AUDIT_ROWS[1];
|
|
293
|
-
if (row === undefined) throw new Error("fixture missing");
|
|
294
|
-
const dispatcher = makeDispatcher({ detail: detailFixtureFor(row) });
|
|
295
|
-
const view = renderDetailScreen(row.id, dispatcher);
|
|
296
|
-
await waitFor(() => {
|
|
297
|
-
const cell = view.queryByTestId("audit-detail-actor");
|
|
298
|
-
if (cell === null) throw new Error("actor field not rendered yet");
|
|
299
|
-
});
|
|
300
|
-
expect(view.getByTestId("audit-detail-actor").textContent).toBe("bob@example.com");
|
|
301
|
-
});
|
|
302
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// @runtime client
|
|
2
|
-
// Single audit event detail. Route entityId = event-store id (bigint string).
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
formatWhen,
|
|
6
|
-
useDispatcher,
|
|
7
|
-
useNav,
|
|
8
|
-
usePrimitives,
|
|
9
|
-
useTranslation,
|
|
10
|
-
} from "@cosmicdrift/kumiko-renderer";
|
|
11
|
-
import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
|
|
12
|
-
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
13
|
-
import { TenantQueries } from "../../tenant/constants";
|
|
14
|
-
import { ANONYMOUS_USER_ID, AuditQueries, SYSTEM_ACTOR_IDS } from "../constants";
|
|
15
|
-
|
|
16
|
-
type AuditDetail = {
|
|
17
|
-
readonly id: string;
|
|
18
|
-
readonly aggregateId: string;
|
|
19
|
-
readonly aggregateType: string;
|
|
20
|
-
readonly type: string;
|
|
21
|
-
readonly createdBy: string;
|
|
22
|
-
readonly createdAt: string;
|
|
23
|
-
readonly payload: Record<string, unknown>;
|
|
24
|
-
readonly metadata: Record<string, unknown>;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type MemberRow = {
|
|
28
|
-
readonly userId: string;
|
|
29
|
-
readonly email: string | null;
|
|
30
|
-
readonly displayName: string | null;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type State =
|
|
34
|
-
| { readonly kind: "loading" }
|
|
35
|
-
| { readonly kind: "missing" }
|
|
36
|
-
| { readonly kind: "error"; readonly message: string }
|
|
37
|
-
| {
|
|
38
|
-
readonly kind: "ready";
|
|
39
|
-
readonly event: AuditDetail;
|
|
40
|
-
/** Resolved member display string, or "" if unknown. System actor is
|
|
41
|
-
* flagged separately so the translated label is formed in render
|
|
42
|
-
* (avoids refetch on locale change — fw#2297). */
|
|
43
|
-
readonly memberLabel: string;
|
|
44
|
-
readonly isSystemActor: boolean;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export function AuditLogDetailScreen(): ReactNode {
|
|
48
|
-
const t = useTranslation();
|
|
49
|
-
const { Banner, Card, Text } = usePrimitives();
|
|
50
|
-
const dispatcher = useDispatcher();
|
|
51
|
-
const nav = useNav();
|
|
52
|
-
const eventId = nav.route?.entityId;
|
|
53
|
-
const [state, setState] = useState<State>({ kind: "loading" });
|
|
54
|
-
|
|
55
|
-
const load = useCallback(async (): Promise<void> => {
|
|
56
|
-
if (eventId === undefined || eventId === "") {
|
|
57
|
-
setState({ kind: "missing" });
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
setState({ kind: "loading" });
|
|
61
|
-
const res = await dispatcher.query<AuditDetail | null>(AuditQueries.details, { id: eventId });
|
|
62
|
-
if (!res.isSuccess) {
|
|
63
|
-
setState({ kind: "error", message: res.error.message });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (res.data === null) {
|
|
67
|
-
setState({ kind: "missing" });
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const event = res.data;
|
|
71
|
-
if (SYSTEM_ACTOR_IDS.has(event.createdBy)) {
|
|
72
|
-
setState({ kind: "ready", event, memberLabel: "", isSystemActor: true });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// Single-user filter — do not decrypt the whole tenant roster for one name.
|
|
76
|
-
const membersRes = await dispatcher.query<readonly MemberRow[]>(TenantQueries.members, {
|
|
77
|
-
userId: event.createdBy,
|
|
78
|
-
});
|
|
79
|
-
const member = membersRes.isSuccess
|
|
80
|
-
? membersRes.data.find((m) => m.userId === event.createdBy)
|
|
81
|
-
: undefined;
|
|
82
|
-
setState({
|
|
83
|
-
kind: "ready",
|
|
84
|
-
event,
|
|
85
|
-
memberLabel: member ? (member.displayName ?? member.email ?? "") : "",
|
|
86
|
-
isSystemActor: false,
|
|
87
|
-
});
|
|
88
|
-
}, [dispatcher, eventId]);
|
|
89
|
-
|
|
90
|
-
// kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
void load();
|
|
93
|
-
}, [load]);
|
|
94
|
-
|
|
95
|
-
if (state.kind === "loading") {
|
|
96
|
-
return (
|
|
97
|
-
<FormScreenShell testId="audit-log-detail-screen" maxWidth="3xl">
|
|
98
|
-
<Text variant="small">{t("audit.log.detail.loading")}</Text>
|
|
99
|
-
</FormScreenShell>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (state.kind === "missing") {
|
|
104
|
-
return (
|
|
105
|
-
<FormScreenShell testId="audit-log-detail-screen" maxWidth="3xl">
|
|
106
|
-
<Banner variant="error">{t("audit.log.detail.missing")}</Banner>
|
|
107
|
-
</FormScreenShell>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (state.kind === "error") {
|
|
112
|
-
return (
|
|
113
|
-
<FormScreenShell testId="audit-log-detail-screen" maxWidth="3xl">
|
|
114
|
-
<Banner variant="error">{state.message}</Banner>
|
|
115
|
-
</FormScreenShell>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const { event, memberLabel, isSystemActor } = state;
|
|
120
|
-
const actorName = isSystemActor
|
|
121
|
-
? t("audit.log.actor.system")
|
|
122
|
-
: event.createdBy === ANONYMOUS_USER_ID
|
|
123
|
-
? t("audit.log.actor.anonymous")
|
|
124
|
-
: memberLabel || t("audit.log.actor.unknown");
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<FormScreenShell
|
|
128
|
-
testId="audit-log-detail-screen"
|
|
129
|
-
maxWidth="3xl"
|
|
130
|
-
className="flex flex-col gap-6"
|
|
131
|
-
>
|
|
132
|
-
<Card slots={{ title: event.type }}>
|
|
133
|
-
<dl className="grid gap-3 text-sm">
|
|
134
|
-
<div>
|
|
135
|
-
<dt className="font-medium">{t("audit.log.col.when")}</dt>
|
|
136
|
-
<dd data-testid="audit-detail-when">{formatWhen(event.createdAt)}</dd>
|
|
137
|
-
</div>
|
|
138
|
-
<div>
|
|
139
|
-
<dt className="font-medium">{t("audit.log.col.aggregate")}</dt>
|
|
140
|
-
<dd data-testid="audit-detail-aggregate">
|
|
141
|
-
{event.aggregateType} / {event.aggregateId}
|
|
142
|
-
</dd>
|
|
143
|
-
</div>
|
|
144
|
-
<div>
|
|
145
|
-
<dt className="font-medium">{t("audit.log.col.actor")}</dt>
|
|
146
|
-
<dd data-testid="audit-detail-actor">{actorName}</dd>
|
|
147
|
-
</div>
|
|
148
|
-
<div>
|
|
149
|
-
<dt className="font-medium">{t("audit.log.detail.field.id")}</dt>
|
|
150
|
-
<dd>
|
|
151
|
-
<Text variant="code">{event.id}</Text>
|
|
152
|
-
</dd>
|
|
153
|
-
</div>
|
|
154
|
-
</dl>
|
|
155
|
-
</Card>
|
|
156
|
-
|
|
157
|
-
<Card slots={{ title: t("audit.log.detail.payload") }}>
|
|
158
|
-
<pre
|
|
159
|
-
className="max-h-96 overflow-auto whitespace-pre-wrap break-all text-xs"
|
|
160
|
-
data-testid="audit-detail-payload"
|
|
161
|
-
>
|
|
162
|
-
{JSON.stringify(event.payload, null, 2)}
|
|
163
|
-
</pre>
|
|
164
|
-
</Card>
|
|
165
|
-
|
|
166
|
-
<Card slots={{ title: t("audit.log.detail.metadata") }}>
|
|
167
|
-
<pre className="max-h-96 overflow-auto whitespace-pre-wrap break-all text-xs">
|
|
168
|
-
{JSON.stringify(event.metadata, null, 2)}
|
|
169
|
-
</pre>
|
|
170
|
-
</Card>
|
|
171
|
-
</FormScreenShell>
|
|
172
|
-
);
|
|
173
|
-
}
|