@cosmicdrift/kumiko-bundled-features 0.240.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 +9 -9
- 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/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/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 -169
- 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,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
|
-
}
|