@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.
Files changed (68) hide show
  1. package/package.json +11 -11
  2. package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
  3. package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
  4. package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
  5. package/src/admin-shell/changes.json +6 -0
  6. package/src/admin-shell/feature.ts +60 -6
  7. package/src/admin-shell/i18n.ts +0 -7
  8. package/src/admin-shell/web/client-plugin.tsx +1 -11
  9. package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
  10. package/src/audit/changes.json +7 -0
  11. package/src/audit/feature.ts +57 -7
  12. package/src/audit/handlers/list.query.ts +50 -14
  13. package/src/audit/i18n.ts +3 -18
  14. package/src/audit/web/client-plugin.tsx +1 -7
  15. package/src/audit/web/index.ts +0 -2
  16. package/src/auth-mfa/web/mfa-enable-screen.tsx +1 -5
  17. package/src/config/__tests__/config.integration.test.ts +31 -0
  18. package/src/config/handlers/readiness.query.ts +8 -1
  19. package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
  20. package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
  21. package/src/jobs/changes.json +7 -0
  22. package/src/jobs/feature.ts +101 -9
  23. package/src/jobs/handlers/list.query.ts +21 -4
  24. package/src/jobs/handlers/trigger.write.ts +16 -3
  25. package/src/jobs/i18n.ts +3 -25
  26. package/src/jobs/index.ts +1 -0
  27. package/src/jobs/web/client-plugin.tsx +1 -7
  28. package/src/jobs/web/index.ts +0 -2
  29. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +1 -1
  30. package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
  31. package/src/tenant/handlers/list.query.ts +1 -0
  32. package/src/tier-engine/__tests__/drift.test.ts +1 -1
  33. package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
  34. package/src/tier-engine/changes.json +9 -1
  35. package/src/tier-engine/constants.ts +5 -5
  36. package/src/tier-engine/feature.ts +36 -10
  37. package/src/tier-engine/i18n.ts +19 -22
  38. package/src/tier-engine/web/client-plugin.tsx +5 -11
  39. package/src/tier-engine/web/index.ts +0 -1
  40. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
  41. package/src/user-data-rights/changes.json +7 -0
  42. package/src/user-data-rights/constants.ts +20 -6
  43. package/src/user-data-rights/feature.ts +113 -14
  44. package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
  45. package/src/user-data-rights/web/client-plugin.tsx +19 -21
  46. package/src/user-data-rights/web/i18n.ts +6 -11
  47. package/src/user-data-rights/web/index.ts +1 -1
  48. package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
  49. package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
  50. package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
  51. package/src/user-profile/changes.json +7 -0
  52. package/src/user-profile/constants.ts +15 -7
  53. package/src/user-profile/feature.ts +96 -9
  54. package/src/user-profile/i18n.ts +8 -12
  55. package/src/user-profile/web/client-plugin.ts +22 -5
  56. package/src/user-profile/web/index.ts +1 -1
  57. package/src/user-profile/web/profile-screen.tsx +122 -265
  58. package/src/admin-shell/web/overview-layout.tsx +0 -69
  59. package/src/admin-shell/web/overview-query.ts +0 -15
  60. package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
  61. package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
  62. package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
  63. package/src/audit/web/audit-log-detail-screen.tsx +0 -173
  64. package/src/audit/web/audit-log-screen.tsx +0 -290
  65. package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
  66. package/src/jobs/web/job-run-detail-screen.tsx +0 -181
  67. package/src/jobs/web/job-runs-screen.tsx +0 -290
  68. package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
@@ -1,290 +0,0 @@
1
- // @runtime client
2
- // SystemAdmin job-run list + manual trigger panel.
3
-
4
- import {
5
- type DataTableSort,
6
- formatWhen,
7
- sortByAccessor,
8
- useMutation,
9
- useNav,
10
- usePrimitives,
11
- useQuery,
12
- useTranslation,
13
- } from "@cosmicdrift/kumiko-renderer";
14
- import { type ReactNode, useMemo, useState } from "react";
15
- import { JOB_RUN_DETAIL_SCREEN_ID, JobHandlers, JobQueries } from "../constants";
16
-
17
- type JobRunRow = {
18
- readonly id: string;
19
- readonly jobName: string;
20
- readonly status: string;
21
- readonly startedAt: string;
22
- readonly finishedAt?: string | null;
23
- readonly duration?: number | null;
24
- readonly error?: string | null;
25
- };
26
-
27
- type ListResponse = { readonly rows: readonly JobRunRow[] };
28
-
29
- type CatalogEntry = {
30
- readonly jobName: string;
31
- readonly perTenant: boolean;
32
- readonly payloadSchema: Record<string, unknown> | null;
33
- };
34
-
35
- type CatalogResponse = { readonly rows: readonly CatalogEntry[] };
36
-
37
- type TriggerResult = { readonly jobName: string; readonly bullJobId: string };
38
-
39
- const STATUS_FILTER_OPTIONS = [
40
- { value: "", labelKey: "jobs.runs.filter.all" },
41
- { value: "completed", labelKey: "jobs.runs.filter.completed" },
42
- { value: "failed", labelKey: "jobs.runs.filter.failed" },
43
- { value: "running", labelKey: "jobs.runs.filter.running" },
44
- { value: "queued", labelKey: "jobs.runs.filter.queued" },
45
- ] as const;
46
-
47
- export function JobRunsScreen(): ReactNode {
48
- const t = useTranslation();
49
- const { Banner, Button, DataTable, Field, Form, Heading, Input, Text } = usePrimitives();
50
- const nav = useNav();
51
- const [statusFilter, setStatusFilter] = useState("");
52
- const [sort, setSort] = useState<DataTableSort | null>(null);
53
- const [jobName, setJobName] = useState("");
54
- const [payloadText, setPayloadText] = useState("{}");
55
- const [clientError, setClientError] = useState<string | null>(null);
56
- const [successMessage, setSuccessMessage] = useState<string | null>(null);
57
-
58
- const listPayload = useMemo(
59
- () => ({
60
- limit: 50,
61
- ...(statusFilter !== "" && {
62
- status: statusFilter as "queued" | "running" | "completed" | "failed",
63
- }),
64
- }),
65
- [statusFilter],
66
- );
67
-
68
- const listQuery = useQuery<ListResponse | null>(JobQueries.list, listPayload);
69
- const catalogQuery = useQuery<CatalogResponse | null>(JobQueries.catalog, {});
70
- const triggerMutation = useMutation<TriggerResult>(JobHandlers.trigger);
71
-
72
- const filterOptions = STATUS_FILTER_OPTIONS.map((opt) => ({
73
- value: opt.value,
74
- label: t(opt.labelKey),
75
- }));
76
-
77
- const catalogRows = catalogQuery.data?.rows ?? [];
78
- const selected = catalogRows.find((row) => row.jobName === jobName) ?? null;
79
- const jobOptions = catalogRows.map((row) => ({
80
- value: row.jobName,
81
- label: row.jobName,
82
- }));
83
-
84
- const schemaHint =
85
- selected?.payloadSchema !== undefined && selected.payloadSchema !== null
86
- ? JSON.stringify(selected.payloadSchema, null, 2)
87
- : null;
88
-
89
- // Switching jobs invalidates any typed payload/messages against the new
90
- // job's schema — reset so a submit can't validate stale payload text
91
- // against the wrong job.
92
- const handleJobNameChange = (name: string): void => {
93
- setJobName(name);
94
- setPayloadText("{}");
95
- setClientError(null);
96
- setSuccessMessage(null);
97
- };
98
-
99
- const onTrigger = async (): Promise<void> => {
100
- setClientError(null);
101
- setSuccessMessage(null);
102
- if (jobName === "") return;
103
-
104
- let payload: Record<string, unknown> = {};
105
- const trimmed = payloadText.trim();
106
- if (trimmed !== "" && trimmed !== "{}") {
107
- try {
108
- const parsed: unknown = JSON.parse(trimmed);
109
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
110
- setClientError(t("jobs.trigger.payload.invalidJson"));
111
- return;
112
- }
113
- payload = parsed as Record<string, unknown>;
114
- } catch {
115
- setClientError(t("jobs.trigger.payload.invalidJson"));
116
- return;
117
- }
118
- }
119
-
120
- const res = await triggerMutation.mutate({
121
- jobName,
122
- ...(Object.keys(payload).length > 0 ? { payload } : {}),
123
- });
124
- if (!res.isSuccess) {
125
- setClientError(res.error.message);
126
- return;
127
- }
128
- setSuccessMessage(t("jobs.trigger.success"));
129
- void listQuery.refetch();
130
- };
131
-
132
- const openDetail = (id: string): void =>
133
- nav.navigate({ screenId: JOB_RUN_DETAIL_SCREEN_ID, entityId: id });
134
-
135
- const rows = listQuery.data?.rows ?? [];
136
- const listLoading = listQuery.loading === true && listQuery.data === null;
137
- const submitting = triggerMutation.pending;
138
-
139
- return (
140
- <div className="flex flex-col gap-6 p-6" data-testid="job-runs-screen">
141
- <section className="flex flex-col gap-3" data-testid="job-trigger-panel">
142
- <Heading variant="section">{t("jobs.trigger.title")}</Heading>
143
- <Text variant="small">{t("jobs.trigger.explainer")}</Text>
144
-
145
- {catalogQuery.error !== null && (
146
- <Banner variant="error" testId="job-trigger-catalog-error">
147
- {catalogQuery.error.message}
148
- </Banner>
149
- )}
150
-
151
- {catalogQuery.error === null &&
152
- catalogRows.length === 0 &&
153
- catalogQuery.loading !== true ? (
154
- <Text variant="small" testId="job-trigger-empty">
155
- {t("jobs.trigger.empty")}
156
- </Text>
157
- ) : catalogQuery.error !== null ? null : (
158
- <Form
159
- testId="job-trigger-form"
160
- onSubmit={(e) => {
161
- e?.preventDefault();
162
- void onTrigger();
163
- }}
164
- actions={
165
- <Button
166
- type="submit"
167
- disabled={jobName === "" || submitting}
168
- loading={submitting}
169
- testId="job-trigger-submit"
170
- >
171
- {submitting ? t("jobs.trigger.submitting") : t("jobs.trigger.submit")}
172
- </Button>
173
- }
174
- >
175
- <Field id="job-trigger-name" label={t("jobs.trigger.job")} required>
176
- <Input
177
- kind="select"
178
- id="job-trigger-name"
179
- name="job-trigger-name"
180
- value={jobName}
181
- onChange={handleJobNameChange}
182
- options={jobOptions}
183
- />
184
- </Field>
185
-
186
- {selected?.perTenant === true && (
187
- <Text variant="small" testId="job-trigger-per-tenant">
188
- {t("jobs.trigger.perTenant")}
189
- </Text>
190
- )}
191
-
192
- {schemaHint !== null && (
193
- <Text variant="code" testId="job-trigger-schema-hint">
194
- {t("jobs.trigger.payload.hint")}: {schemaHint}
195
- </Text>
196
- )}
197
-
198
- <Field id="job-trigger-payload" label={t("jobs.trigger.payload")}>
199
- <Input
200
- kind="textarea"
201
- id="job-trigger-payload"
202
- name="job-trigger-payload"
203
- value={payloadText}
204
- onChange={setPayloadText}
205
- />
206
- </Field>
207
-
208
- {clientError !== null && (
209
- <Banner variant="error" testId="job-trigger-error">
210
- {clientError}
211
- </Banner>
212
- )}
213
- {successMessage !== null && (
214
- <Banner variant="info" testId="job-trigger-success">
215
- {successMessage}
216
- </Banner>
217
- )}
218
- </Form>
219
- )}
220
- </section>
221
-
222
- {listQuery.error !== null && (
223
- <Banner variant="error" testId="job-runs-list-error">
224
- {listQuery.error.message}
225
- </Banner>
226
- )}
227
-
228
- {listLoading ? (
229
- <Text variant="small">{t("jobs.runs.loading")}</Text>
230
- ) : listQuery.error !== null ? null : (
231
- <DataTable
232
- testId="job-runs-table"
233
- columns={[
234
- { field: "job", label: t("jobs.runs.col.job"), type: "string", sortable: true },
235
- { field: "status", label: t("jobs.runs.col.status"), type: "string", sortable: true },
236
- { field: "started", label: t("jobs.runs.col.started"), type: "string", sortable: true },
237
- {
238
- field: "duration",
239
- label: t("jobs.runs.col.duration"),
240
- type: "string",
241
- sortable: false,
242
- },
243
- ]}
244
- sort={sort}
245
- onSortChange={setSort}
246
- rows={sortByAccessor(rows, sort, SORT_ACCESSORS).map((row) => ({
247
- id: row.id,
248
- values: {
249
- job: row.jobName,
250
- status: row.status,
251
- started: formatWhen(row.startedAt),
252
- duration: row.duration ?? "—",
253
- },
254
- }))}
255
- onRowClick={(row) => openDetail(row.id)}
256
- toolbarStart={
257
- <Field id="job-runs-status-filter" label={t("jobs.runs.filter.status")}>
258
- <Input
259
- kind="select"
260
- id="job-runs-status-filter"
261
- name="job-runs-status-filter"
262
- value={statusFilter}
263
- onChange={setStatusFilter}
264
- options={filterOptions}
265
- />
266
- </Field>
267
- }
268
- rowActions={[
269
- {
270
- id: "open",
271
- label: t("jobs.runs.open"),
272
- style: "secondary",
273
- onTrigger: (row) => openDetail(row.id),
274
- },
275
- ]}
276
- rowActionMode="inline"
277
- emptyState={<Text variant="small">{t("jobs.runs.empty")}</Text>}
278
- />
279
- )}
280
- </div>
281
- );
282
- }
283
-
284
- // Client-sort over the loaded page (≤50 rows). startedAt is an ISO string, so
285
- // lexicographic compare is chronological — no Date parsing needed.
286
- const SORT_ACCESSORS: Record<string, (r: JobRunRow) => string | number> = {
287
- job: (r) => r.jobName,
288
- status: (r) => r.status,
289
- started: (r) => r.startedAt,
290
- };
@@ -1,168 +0,0 @@
1
- // @runtime client
2
- // TierAdminScreen — SystemAdmin weist einem beliebigen Tenant ein Tier zu,
3
- // ohne Billing-Kauf. Tenant-Picker (tenant:query:list) → aktuelles Tier
4
- // (get-tenant-tier) → Tier-Dropdown (tier-options) → Speichern
5
- // (set-tenant-tier). Apps registrieren die Komponente als custom-Screen:
6
- // r.screen({ id: "tier-admin", type: "custom",
7
- // renderer: { react: { __component: "TierAdminScreen" } },
8
- // access: { roles: ["SystemAdmin"] } })
9
-
10
- import {
11
- useDispatcher,
12
- usePrimitives,
13
- useQuery,
14
- useTranslation,
15
- } from "@cosmicdrift/kumiko-renderer";
16
- import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
17
- import { type ReactNode, useEffect, useState } from "react";
18
- import { TierEngineHandlers, TierEngineQueries } from "../constants";
19
-
20
- const TENANT_LIST_QUERY = "tenant:query:list";
21
-
22
- type TenantRow = { readonly id: string; readonly name: string };
23
- type TenantListResponse = { readonly rows: readonly TenantRow[] };
24
- type TierAssignmentRow = { readonly tier: string; readonly source: string | null };
25
- type TierOptionsResponse = { readonly tiers: readonly string[] };
26
- type SetTenantTierResponse = {
27
- readonly tenantId: string;
28
- readonly tier: string;
29
- readonly isNew: boolean;
30
- };
31
-
32
- type Status =
33
- | { kind: "idle" }
34
- | { kind: "submitting" }
35
- | { kind: "success"; tier: string }
36
- | { kind: "error"; messageKey: string };
37
-
38
- export function TierAdminScreen(): ReactNode {
39
- const t = useTranslation();
40
- const { Field, Input, Button, Banner, Form, Text } = usePrimitives();
41
- const dispatcher = useDispatcher();
42
-
43
- // ponytail: nur die erste Seite (default-limit, nextCursor ignoriert) —
44
- // reicht für Apps mit wenigen Tenants (cashcolt). Pagination/Suche
45
- // nachrüsten, wenn ein Operator mit vielen Tenants nicht alle sieht.
46
- const tenantsQuery = useQuery<TenantListResponse | null>(TENANT_LIST_QUERY, {});
47
- const tierOptionsQuery = useQuery<TierOptionsResponse | null>(TierEngineQueries.tierOptions, {});
48
-
49
- const [tenantId, setTenantId] = useState("");
50
- const [tier, setTier] = useState("");
51
- const [status, setStatus] = useState<Status>({ kind: "idle" });
52
-
53
- const currentTierQuery = useQuery<TierAssignmentRow | null>(
54
- TierEngineQueries.getTenantTier,
55
- { tenantId },
56
- { enabled: tenantId !== "" },
57
- );
58
-
59
- // Tenant switch: reset selection + status so no other tenant's tier stays
60
- // shown (UI-level mis-grant guard). tenantId is a pure trigger here (the
61
- // body doesn't read it) — Biome's "extra dep" autofix would empty the deps
62
- // and limit the reset to mount.
63
- // biome-ignore lint/correctness/useExhaustiveDependencies: tenantId is the intended reset trigger, do not remove. kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
64
- useEffect(() => {
65
- setTier("");
66
- setStatus({ kind: "idle" });
67
- }, [tenantId]);
68
-
69
- const tenantOptions = (tenantsQuery.data?.rows ?? []).map((row) => ({
70
- value: row.id,
71
- label: row.name,
72
- }));
73
- const tierOptions = tierOptionsQuery.data?.tiers ?? [];
74
- const currentTier = currentTierQuery.data?.tier ?? null;
75
-
76
- const onSave = async (): Promise<void> => {
77
- if (tenantId === "" || tier === "") return;
78
- setStatus({ kind: "submitting" });
79
- const res = await dispatcher.write<SetTenantTierResponse>(TierEngineHandlers.setTenantTier, {
80
- tenantId,
81
- tier,
82
- });
83
- if (!res.isSuccess) {
84
- setStatus({ kind: "error", messageKey: "tier-admin.error.generic" });
85
- return;
86
- }
87
- setStatus({ kind: "success", tier: res.data.tier });
88
- void currentTierQuery.refetch();
89
- };
90
-
91
- const submitting = status.kind === "submitting";
92
- const canSubmit = tenantId !== "" && tier !== "" && !submitting;
93
-
94
- return (
95
- <FormScreenShell testId="tier-admin-screen" className="flex flex-col gap-6">
96
- {tenantsQuery.error !== null && (
97
- <Banner variant="error" testId="tier-admin-load-error">
98
- {t("tier-admin.error.load")}
99
- </Banner>
100
- )}
101
- {tierOptions.length === 0 && tierOptionsQuery.loading !== true && (
102
- <Banner variant="info" testId="tier-admin-no-tiers">
103
- {t("tier-admin.error.noTiers")}
104
- </Banner>
105
- )}
106
-
107
- <Form
108
- testId="tier-admin-form"
109
- onSubmit={(e) => {
110
- e?.preventDefault();
111
- void onSave();
112
- }}
113
- actions={
114
- <Button
115
- type="submit"
116
- disabled={!canSubmit}
117
- loading={submitting}
118
- testId="tier-admin-submit"
119
- >
120
- {t("tier-admin.submit")}
121
- </Button>
122
- }
123
- >
124
- <Text variant="small">{t("tier-admin.explainer")}</Text>
125
-
126
- <Field id="tier-admin-tenant" label={t("tier-admin.tenant.label")} required>
127
- <Input
128
- kind="select"
129
- id="tier-admin-tenant"
130
- name="tier-admin-tenant"
131
- value={tenantId}
132
- onChange={setTenantId}
133
- options={tenantOptions}
134
- />
135
- </Field>
136
-
137
- {tenantId !== "" && (
138
- <Text variant="small" testId="tier-admin-current">
139
- {t("tier-admin.current.label")}: {currentTier ?? t("tier-admin.current.none")}
140
- </Text>
141
- )}
142
-
143
- <Field id="tier-admin-tier" label={t("tier-admin.tier.label")} required>
144
- <Input
145
- kind="select"
146
- id="tier-admin-tier"
147
- name="tier-admin-tier"
148
- value={tier}
149
- onChange={setTier}
150
- options={tierOptions}
151
- disabled={tierOptions.length === 0}
152
- />
153
- </Field>
154
-
155
- {status.kind === "success" && (
156
- <Banner variant="info" testId="tier-admin-success">
157
- {t("tier-admin.success", { tier: status.tier })}
158
- </Banner>
159
- )}
160
- {status.kind === "error" && (
161
- <Banner variant="error" testId="tier-admin-error">
162
- {t(status.messageKey)}
163
- </Banner>
164
- )}
165
- </Form>
166
- </FormScreenShell>
167
- );
168
- }