@saasicat/ui-vue 0.24.2 → 0.26.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/README.md +32 -8
- package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
- package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
- package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
- package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
- package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
- package/dist/client/index.cjs +758 -83
- package/dist/client/index.d.cts +414 -9
- package/dist/client/index.d.ts +414 -9
- package/dist/client/index.js +84 -12
- package/dist/index.cjs +1325 -294
- package/dist/index.d.cts +205 -15
- package/dist/index.d.ts +205 -15
- package/dist/index.js +566 -240
- package/dist/quasar/index.cjs +574 -62
- package/dist/quasar/index.d.cts +61 -6
- package/dist/quasar/index.d.ts +61 -6
- package/dist/quasar/index.js +83 -19
- package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
- package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
- package/package.json +4 -3
- package/src/client/admin-error.ts +552 -0
- package/src/client/admin-resource-client.ts +9 -12
- package/src/client/batch-column-fetcher.ts +2 -2
- package/src/client/boot-loader.ts +12 -1
- package/src/client/http/axios-http-client.ts +383 -0
- package/src/client/http/fetch-http-client.ts +89 -0
- package/src/client/http/index.ts +6 -0
- package/src/client/http-json.ts +82 -16
- package/src/client/i18n/messages/discovery.ts +0 -6
- package/src/client/i18n/messages/errors.ts +44 -0
- package/src/client/i18n/messages/marketing.ts +0 -6
- package/src/client/i18n/messages/promos.ts +0 -2
- package/src/client/i18n/messages/shell.ts +12 -0
- package/src/client/i18n/messages.ts +3 -0
- package/src/client/index.ts +6 -0
- package/src/client/manifest-loader.ts +65 -15
- package/src/client/resources/audit.resource.ts +25 -0
- package/src/client/resources/define-resource.ts +166 -0
- package/src/client/resources/index.ts +42 -0
- package/src/client/resources/list-resource.ts +176 -0
- package/src/client/resources/plans.resource.ts +169 -0
- package/src/client/resources/resource-request.ts +117 -0
- package/src/client/resources/tenants.resource.ts +27 -0
- package/src/client/types.ts +10 -3
- package/src/components/BundleVersionPublishDialog.vue +1 -1
- package/src/components/MarketingPromotionsTab.vue +263 -241
- package/src/components/ThemeSwitcher.vue +85 -0
- package/src/components/admin-page/AdminAccordion.vue +125 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
- package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
- package/src/components/plan/PlanCycleToggle.vue +1 -1
- package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
- package/src/index.ts +5 -0
- package/src/pages-standard/AdminLayout.vue +57 -0
- package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
- package/src/pages-standard/BundlesPage.vue +5 -35
- package/src/pages-standard/DiscoveryPage.vue +8 -1
- package/src/pages-standard/MarketingCatalogPage.vue +14 -3
- package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
- package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
- package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
- package/src/quasar/confirm.ts +83 -0
- package/src/quasar/create-super-admin-app.ts +84 -10
- package/src/quasar/dark-bridge.ts +47 -10
- package/src/quasar/index.ts +1 -0
- package/src/ui/theme/components/accordion.css +118 -0
- package/src/ui/theme/index.css +1 -0
- package/src/vue/create-admin-routes.ts +9 -1
- package/src/vue/platform-loaders.ts +12 -2
- package/src/vue/resource-registry.ts +211 -0
- package/src/vue/super-admin-context.ts +20 -5
- package/src/vue/ui-confirm.ts +76 -0
- package/src/vue/use-api-list.ts +50 -45
- package/src/vue/use-async-action.ts +170 -0
- package/src/vue/use-async-data.ts +83 -0
- package/src/vue/use-bundles.ts +34 -7
- package/src/vue/use-catalog-entries.ts +37 -12
- package/src/vue/use-discovery.ts +27 -6
- package/src/vue/use-marketing-projections.ts +29 -7
- package/src/vue/use-plans.ts +36 -8
- package/src/vue/use-promotions.ts +27 -7
- package/src/vue/use-resource-list.ts +272 -0
- package/src/vue/use-sa-theme.ts +34 -5
- package/src/vue/use-tenant-subscription-bundles.ts +22 -3
package/dist/client/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(client_exports, {
|
|
|
23
23
|
ADMIN_UI_VERSION: () => ADMIN_UI_VERSION,
|
|
24
24
|
ActionDefNotInManifestError: () => ActionDefNotInManifestError,
|
|
25
25
|
ActionRegistry: () => ActionRegistry,
|
|
26
|
+
AdminError: () => AdminError,
|
|
26
27
|
BatchColumnDriftError: () => BatchColumnDriftError,
|
|
27
28
|
BatchColumnFetcher: () => BatchColumnFetcher,
|
|
28
29
|
BootLoadError: () => BootLoadError,
|
|
@@ -34,6 +35,10 @@ __export(client_exports, {
|
|
|
34
35
|
IDENTITY_ACCENT_VALUES: () => IDENTITY_ACCENT_VALUES,
|
|
35
36
|
IDENTITY_NEUTRAL: () => IDENTITY_NEUTRAL,
|
|
36
37
|
IDENTITY_NEUTRAL_VALUE: () => IDENTITY_NEUTRAL_VALUE,
|
|
38
|
+
LIST_FIRST_PAGE: () => LIST_FIRST_PAGE,
|
|
39
|
+
LIST_PAGE_SIZE_DEFAULT: () => LIST_PAGE_SIZE_DEFAULT,
|
|
40
|
+
LIST_PAGE_SIZE_MAX: () => LIST_PAGE_SIZE_MAX,
|
|
41
|
+
LIST_PAGINATION_PARAMS: () => LIST_PAGINATION_PARAMS,
|
|
37
42
|
ManifestLoadError: () => ManifestLoadError,
|
|
38
43
|
ManifestLoader: () => ManifestLoader,
|
|
39
44
|
MissingHandlerError: () => MissingHandlerError,
|
|
@@ -41,32 +46,63 @@ __export(client_exports, {
|
|
|
41
46
|
SA_LOCALES: () => SA_LOCALES,
|
|
42
47
|
SA_LOCALE_LABELS: () => SA_LOCALE_LABELS,
|
|
43
48
|
SA_MESSAGES: () => SA_MESSAGES,
|
|
49
|
+
adminErrorMessage: () => adminErrorMessage,
|
|
50
|
+
auditResource: () => auditResource,
|
|
51
|
+
bindResource: () => bindResource,
|
|
44
52
|
buildRoutes: () => buildRoutes,
|
|
45
53
|
buildSidebar: () => buildSidebar,
|
|
54
|
+
clampListPage: () => clampListPage,
|
|
55
|
+
clampListPageSize: () => clampListPageSize,
|
|
46
56
|
countPlans: () => countPlans,
|
|
47
57
|
createAdminResourceClient: () => createAdminResourceClient,
|
|
58
|
+
createAxiosHttpClient: () => createAxiosHttpClient,
|
|
59
|
+
createFetchHttpClient: () => createFetchHttpClient,
|
|
48
60
|
createSaCatalog: () => createSaCatalog,
|
|
49
61
|
defaultHttpClient: () => defaultHttpClient,
|
|
50
62
|
defaultKvStore: () => defaultKvStore,
|
|
51
63
|
defaultSectionOrder: () => defaultSectionOrder,
|
|
64
|
+
defineListOp: () => defineListOp,
|
|
52
65
|
defineMessages: () => defineMessages,
|
|
66
|
+
defineResource: () => defineResource,
|
|
67
|
+
filterQueryString: () => filterQueryString,
|
|
53
68
|
formatCurrency: () => formatCurrency,
|
|
54
69
|
formatMessage: () => formatMessage,
|
|
55
70
|
getJson: () => getJson,
|
|
56
71
|
identityAccentAt: () => identityAccentAt,
|
|
57
72
|
identityAccentFor: () => identityAccentFor,
|
|
58
73
|
identityChipStyle: () => identityChipStyle,
|
|
74
|
+
isAdminError: () => isAdminError,
|
|
75
|
+
isAxiosNoResponseError: () => isAxiosNoResponseError,
|
|
59
76
|
isCurrentlyValid: () => isCurrentlyValid,
|
|
77
|
+
isEmptyResponse: () => isEmptyResponse,
|
|
60
78
|
isExpired: () => isExpired,
|
|
61
79
|
isFutureScheduled: () => isFutureScheduled,
|
|
80
|
+
isPlatformError: () => isPlatformError,
|
|
62
81
|
isProductionBoot: () => isProductionBoot,
|
|
63
82
|
isSaBuiltinLocale: () => isSaBuiltinLocale,
|
|
83
|
+
isSentInQuery: () => isSentInQuery,
|
|
84
|
+
isTransportFailure: () => isTransportFailure,
|
|
85
|
+
listUrl: () => listUrl,
|
|
86
|
+
markEmptyResponse: () => markEmptyResponse,
|
|
87
|
+
markPlatformError: () => markPlatformError,
|
|
88
|
+
markTransportFailure: () => markTransportFailure,
|
|
64
89
|
mergeMessages: () => mergeMessages,
|
|
90
|
+
planVersionsResource: () => planVersionsResource,
|
|
91
|
+
plansResource: () => plansResource,
|
|
92
|
+
platformResources: () => platformResources,
|
|
65
93
|
postJson: () => postJson,
|
|
94
|
+
readErrorCode: () => readErrorCode,
|
|
95
|
+
readErrorDetail: () => readErrorDetail,
|
|
96
|
+
readListPage: () => readListPage,
|
|
97
|
+
requestJson: () => requestJson,
|
|
98
|
+
requestJsonBody: () => requestJsonBody,
|
|
99
|
+
requireServerAnswer: () => requireServerAnswer,
|
|
66
100
|
resolveExtension: () => resolveExtension,
|
|
67
101
|
resolveLoginBranding: () => resolveLoginBranding,
|
|
68
102
|
resolveMessages: () => resolveMessages,
|
|
69
103
|
resolvePlans: () => resolvePlans,
|
|
104
|
+
tenantsResource: () => tenantsResource,
|
|
105
|
+
toAdminError: () => toAdminError,
|
|
70
106
|
todayIsoDate: () => todayIsoDate,
|
|
71
107
|
trimTrailingSlashes: () => trimTrailingSlashes
|
|
72
108
|
});
|
|
@@ -75,6 +111,287 @@ module.exports = __toCommonJS(client_exports);
|
|
|
75
111
|
// src/client/version.ts
|
|
76
112
|
var ADMIN_UI_VERSION = "1.2.0";
|
|
77
113
|
|
|
114
|
+
// src/client/i18n/format.ts
|
|
115
|
+
var import_types = require("@saasicat/types");
|
|
116
|
+
function formatMessage(template, params) {
|
|
117
|
+
return (0, import_types.formatErrorMessage)(template, params);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/client/admin-error.ts
|
|
121
|
+
var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
|
|
122
|
+
var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
|
|
123
|
+
function markPlatformError(error) {
|
|
124
|
+
Object.defineProperty(error, PLATFORM_ERROR, { value: true });
|
|
125
|
+
}
|
|
126
|
+
function isPlatformError(value) {
|
|
127
|
+
return typeof value === "object" && value !== null && PLATFORM_ERROR in value;
|
|
128
|
+
}
|
|
129
|
+
var EMPTY_RESPONSE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/EmptyResponse");
|
|
130
|
+
function markEmptyResponse(error) {
|
|
131
|
+
Object.defineProperty(error, EMPTY_RESPONSE, { value: true });
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
function isEmptyResponse(value) {
|
|
135
|
+
return typeof value === "object" && value !== null && EMPTY_RESPONSE in value;
|
|
136
|
+
}
|
|
137
|
+
var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
|
|
138
|
+
function markTransportFailure(error) {
|
|
139
|
+
if (typeof error === "object" && error !== null) {
|
|
140
|
+
try {
|
|
141
|
+
Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
function isTransportFailure(value) {
|
|
148
|
+
return typeof value === "object" && value !== null && TRANSPORT_FAILURE in value;
|
|
149
|
+
}
|
|
150
|
+
function describe(init) {
|
|
151
|
+
const status = init.status ?? 0;
|
|
152
|
+
const where = [init.method, init.url].filter(Boolean).join(" ");
|
|
153
|
+
const what = status > 0 ? `HTTP ${status}` : "request failed";
|
|
154
|
+
const head = where ? `${where} \u2192 ${what}` : what;
|
|
155
|
+
const code = init.code ? ` (${init.code})` : "";
|
|
156
|
+
return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
|
|
157
|
+
}
|
|
158
|
+
var AdminError = class extends Error {
|
|
159
|
+
/** HTTP status. `0` means the request never produced one. */
|
|
160
|
+
status;
|
|
161
|
+
/** Machine-readable code from the response body, when it carried one. */
|
|
162
|
+
code;
|
|
163
|
+
/** Parsed response body, when there was one. */
|
|
164
|
+
body;
|
|
165
|
+
/** Requested URL, when known. */
|
|
166
|
+
url;
|
|
167
|
+
/** Request method, when known. */
|
|
168
|
+
method;
|
|
169
|
+
/**
|
|
170
|
+
* Text the failing side supplied — the only candidate for user-facing
|
|
171
|
+
* output. `undefined` when nothing was supplied.
|
|
172
|
+
*/
|
|
173
|
+
detail;
|
|
174
|
+
/** The request completed but the answer was unusable. See `AdminErrorInit`. */
|
|
175
|
+
emptyResponse;
|
|
176
|
+
/** The request never reached the server. See `AdminErrorInit`. */
|
|
177
|
+
transportFailure;
|
|
178
|
+
constructor(init = {}) {
|
|
179
|
+
super(init.message ?? describe(init), { cause: init.cause });
|
|
180
|
+
this.name = "AdminError";
|
|
181
|
+
this.status = init.status ?? 0;
|
|
182
|
+
this.code = init.code;
|
|
183
|
+
this.body = init.body;
|
|
184
|
+
this.url = init.url;
|
|
185
|
+
this.method = init.method;
|
|
186
|
+
this.detail = init.detail;
|
|
187
|
+
this.emptyResponse = init.emptyResponse ?? false;
|
|
188
|
+
this.transportFailure = init.transportFailure ?? false;
|
|
189
|
+
Object.defineProperty(this, ADMIN_ERROR, { value: true });
|
|
190
|
+
markPlatformError(this);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function isAdminError(err) {
|
|
194
|
+
return typeof err === "object" && err !== null && ADMIN_ERROR in err;
|
|
195
|
+
}
|
|
196
|
+
function asRecord(value) {
|
|
197
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
198
|
+
}
|
|
199
|
+
function asString(value) {
|
|
200
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
201
|
+
}
|
|
202
|
+
function isAxiosNoResponseError(value) {
|
|
203
|
+
const record = asRecord(value);
|
|
204
|
+
if (!record) return false;
|
|
205
|
+
return record.isAxiosError === true && record.request != null && record.response == null;
|
|
206
|
+
}
|
|
207
|
+
function readErrorDetail(body) {
|
|
208
|
+
const record = asRecord(body);
|
|
209
|
+
if (!record) return asString(body);
|
|
210
|
+
const message = record.message;
|
|
211
|
+
if (Array.isArray(message)) {
|
|
212
|
+
const parts = message.filter((part) => typeof part === "string");
|
|
213
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
214
|
+
}
|
|
215
|
+
return asString(message) ?? asString(record.error) ?? asString(record.reason);
|
|
216
|
+
}
|
|
217
|
+
function readErrorCode(body) {
|
|
218
|
+
return asString(asRecord(body)?.code);
|
|
219
|
+
}
|
|
220
|
+
function toAdminError(err) {
|
|
221
|
+
if (isAdminError(err)) return err;
|
|
222
|
+
const record = asRecord(err);
|
|
223
|
+
const response = asRecord(record?.response);
|
|
224
|
+
if (response && typeof response.status === "number") {
|
|
225
|
+
const config = asRecord(record?.config);
|
|
226
|
+
const body = response.data;
|
|
227
|
+
return new AdminError({
|
|
228
|
+
status: response.status,
|
|
229
|
+
code: readErrorCode(body),
|
|
230
|
+
body,
|
|
231
|
+
url: asString(config?.url),
|
|
232
|
+
method: asString(config?.method)?.toUpperCase(),
|
|
233
|
+
// Only what the response actually said. `err.message` here is
|
|
234
|
+
// axios's own generated line ("Request failed with status code
|
|
235
|
+
// 401") — English, untranslated, and about the transport rather
|
|
236
|
+
// than about what went wrong. Letting it through as `detail` would
|
|
237
|
+
// make `adminErrorMessage` return it in preference to the
|
|
238
|
+
// localized wording, which is the whole point of that wording.
|
|
239
|
+
detail: readErrorDetail(body),
|
|
240
|
+
cause: err
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (record && typeof record.status === "number") {
|
|
244
|
+
const ours = isPlatformError(err);
|
|
245
|
+
return new AdminError({
|
|
246
|
+
status: record.status,
|
|
247
|
+
code: readErrorCode(record.body) ?? asString(record.code),
|
|
248
|
+
body: record.body,
|
|
249
|
+
// Same reasoning as above: `BundlesApiError`'s message is
|
|
250
|
+
// "Bundles API responded with HTTP 403", a diagnostic. It stays on
|
|
251
|
+
// `message`, where logs read it.
|
|
252
|
+
detail: readErrorDetail(record.body) ?? (ours ? void 0 : asString(record.message)),
|
|
253
|
+
// Asked of the throw site, not of the class: only the site that
|
|
254
|
+
// read the response knows whether one arrived. `status === 0 &&
|
|
255
|
+
// ours` was the same question put to the wrong witness — the
|
|
256
|
+
// loaders raise branded errors from a status the client chose, so
|
|
257
|
+
// a client that resolves a transport failure as `0` turned a
|
|
258
|
+
// failed GET into "the change may have been applied".
|
|
259
|
+
emptyResponse: isEmptyResponse(err),
|
|
260
|
+
transportFailure: isTransportFailure(err),
|
|
261
|
+
message: asString(record.message),
|
|
262
|
+
cause: err
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (err instanceof Error) {
|
|
266
|
+
const transport = isTransportFailure(err) || isAxiosNoResponseError(err);
|
|
267
|
+
const saidSomethingReadable = !transport && !(err instanceof TypeError);
|
|
268
|
+
return new AdminError({
|
|
269
|
+
detail: saidSomethingReadable ? asString(err.message) : void 0,
|
|
270
|
+
transportFailure: transport,
|
|
271
|
+
message: err.message,
|
|
272
|
+
cause: err
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const text = asString(err) ?? asString(record?.message);
|
|
276
|
+
return new AdminError({
|
|
277
|
+
code: asString(record?.code),
|
|
278
|
+
detail: text,
|
|
279
|
+
message: text,
|
|
280
|
+
cause: err
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function statusKey(status) {
|
|
284
|
+
if (status === 400 || status === 422) return "validation";
|
|
285
|
+
if (status === 401) return "unauthorized";
|
|
286
|
+
if (status === 403) return "forbidden";
|
|
287
|
+
if (status === 404) return "notFound";
|
|
288
|
+
if (status === 409) return "conflict";
|
|
289
|
+
if (status === 429) return "rateLimited";
|
|
290
|
+
if (status >= 500) return "server";
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
function adminErrorMessage(err, msgs) {
|
|
294
|
+
const error = toAdminError(err);
|
|
295
|
+
if (error.detail) return error.detail;
|
|
296
|
+
if (error.emptyResponse) return msgs.emptyResponse;
|
|
297
|
+
if (error.transportFailure) return msgs.network;
|
|
298
|
+
if (error.status > 0) {
|
|
299
|
+
const key = statusKey(error.status);
|
|
300
|
+
return key ? msgs[key] : formatMessage(msgs.httpStatus, { status: error.status });
|
|
301
|
+
}
|
|
302
|
+
return msgs.unexpected;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/client/http-json.ts
|
|
306
|
+
function serverAnswered(status) {
|
|
307
|
+
return status > 0;
|
|
308
|
+
}
|
|
309
|
+
function requireServerAnswer(status, method, url, raise) {
|
|
310
|
+
if (serverAnswered(status)) return;
|
|
311
|
+
throw markTransportFailure(
|
|
312
|
+
raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
var HttpJsonError = AdminError;
|
|
316
|
+
async function readErrorBody(res) {
|
|
317
|
+
try {
|
|
318
|
+
return await res.json();
|
|
319
|
+
} catch {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function failed(res, method, url) {
|
|
324
|
+
const body = await readErrorBody(res);
|
|
325
|
+
return new AdminError({
|
|
326
|
+
status: res.status,
|
|
327
|
+
// Read here rather than left to the status: `status: 0` reaches
|
|
328
|
+
// `adminErrorMessage` as "nothing is known about this failure", and
|
|
329
|
+
// this seam knows better — it held the response.
|
|
330
|
+
transportFailure: !serverAnswered(res.status),
|
|
331
|
+
code: readErrorCode(body),
|
|
332
|
+
body,
|
|
333
|
+
url,
|
|
334
|
+
method,
|
|
335
|
+
// Shared with `toAdminError` on purpose: a NestJS `ValidationPipe`
|
|
336
|
+
// rejection carries `message` as an array, and reading it here in a
|
|
337
|
+
// second, narrower way is how those constraints went missing.
|
|
338
|
+
detail: readErrorDetail(body)
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function trimTrailingSlashes(url) {
|
|
342
|
+
let end = url.length;
|
|
343
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
344
|
+
return url.slice(0, end);
|
|
345
|
+
}
|
|
346
|
+
async function getJson(http, url) {
|
|
347
|
+
const res = await http(url);
|
|
348
|
+
if (res.status < 200 || res.status >= 300) {
|
|
349
|
+
throw await failed(res, "GET", url);
|
|
350
|
+
}
|
|
351
|
+
return await res.json();
|
|
352
|
+
}
|
|
353
|
+
async function postJson(http, url, body) {
|
|
354
|
+
const res = await http(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "Content-Type": "application/json" },
|
|
357
|
+
body: JSON.stringify(body)
|
|
358
|
+
});
|
|
359
|
+
if (res.status < 200 || res.status >= 300) {
|
|
360
|
+
throw await failed(res, "POST", url);
|
|
361
|
+
}
|
|
362
|
+
return await res.json();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/client/http/fetch-http-client.ts
|
|
366
|
+
var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
|
|
367
|
+
function resolveUrl(url, baseUrl) {
|
|
368
|
+
if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
|
|
369
|
+
return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
370
|
+
}
|
|
371
|
+
function createFetchHttpClient(options = {}) {
|
|
372
|
+
return async (url, init) => {
|
|
373
|
+
const headers = new Headers(await options.headers?.());
|
|
374
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
375
|
+
for (const [name, value] of Object.entries(init?.headers ?? {})) {
|
|
376
|
+
headers.set(name, value);
|
|
377
|
+
}
|
|
378
|
+
if (init?.body !== void 0 && !headers.has("Content-Type")) {
|
|
379
|
+
headers.set("Content-Type", "application/json");
|
|
380
|
+
}
|
|
381
|
+
let response;
|
|
382
|
+
try {
|
|
383
|
+
response = await fetch(resolveUrl(url, options.baseUrl), {
|
|
384
|
+
method: init?.method ?? "GET",
|
|
385
|
+
headers,
|
|
386
|
+
body: init?.body
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw markTransportFailure(err);
|
|
390
|
+
}
|
|
391
|
+
return response;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
78
395
|
// src/client/types.ts
|
|
79
396
|
var NOOP_KV_STORE = {
|
|
80
397
|
get: () => null,
|
|
@@ -116,51 +433,326 @@ function resolveLocalStorage() {
|
|
|
116
433
|
}
|
|
117
434
|
}
|
|
118
435
|
function defaultHttpClient() {
|
|
119
|
-
return (
|
|
436
|
+
return createFetchHttpClient();
|
|
120
437
|
}
|
|
121
438
|
|
|
122
|
-
// src/client/http-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
439
|
+
// src/client/http/axios-http-client.ts
|
|
440
|
+
function stripped(url, prefixes) {
|
|
441
|
+
for (const prefix of prefixes) {
|
|
442
|
+
const trimmed = trimTrailingSlashes(prefix);
|
|
443
|
+
if (!trimmed || !url.startsWith(trimmed)) continue;
|
|
444
|
+
const rest = url.slice(trimmed.length);
|
|
445
|
+
if (rest === "") return "/";
|
|
446
|
+
if (rest.startsWith("/")) return rest;
|
|
447
|
+
if (rest.startsWith("?") || rest.startsWith("#")) return `/${rest}`;
|
|
129
448
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
449
|
+
return url;
|
|
450
|
+
}
|
|
451
|
+
function headerReader(headers) {
|
|
452
|
+
const record = typeof headers === "object" && headers !== null ? headers : {};
|
|
453
|
+
const lowered = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const [key, value] of Object.entries(record)) lowered.set(key.toLowerCase(), value);
|
|
455
|
+
return (name) => {
|
|
456
|
+
const value = lowered.get(name.toLowerCase());
|
|
457
|
+
return value == null ? null : String(value);
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function bodyIsRaw(response, declared) {
|
|
461
|
+
if (declared !== "auto") return declared === "raw";
|
|
462
|
+
if (response.data === "") return true;
|
|
463
|
+
const config = response.config;
|
|
464
|
+
if (config?.transformResponse === null) return true;
|
|
465
|
+
if (Array.isArray(config?.transformResponse) && config.transformResponse.length === 0) {
|
|
466
|
+
return true;
|
|
139
467
|
}
|
|
468
|
+
const responseType = config?.responseType;
|
|
469
|
+
if (responseType === "json") return false;
|
|
470
|
+
if (typeof responseType === "string" && responseType !== "") return true;
|
|
471
|
+
const transitional = config?.transitional;
|
|
472
|
+
return typeof transitional === "object" && transitional !== null && transitional.forcedJSONParsing === false;
|
|
140
473
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
474
|
+
async function readBody(data) {
|
|
475
|
+
if (typeof data === "string" || data === null || data === void 0) return data;
|
|
476
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
477
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
|
|
478
|
+
const maybe = data;
|
|
479
|
+
if (typeof maybe.pipe === "function" || typeof maybe.getReader === "function") {
|
|
480
|
+
throw new TypeError(
|
|
481
|
+
"createAxiosHttpClient cannot read a streamed body: responseType 'stream' can be consumed only once, and never synchronously. Use 'arraybuffer' or the default for the instance the platform receives."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (typeof maybe.text === "function") return maybe.text.call(data);
|
|
485
|
+
return data;
|
|
145
486
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
487
|
+
function adapt(response, declared) {
|
|
488
|
+
return {
|
|
489
|
+
status: response.status,
|
|
490
|
+
headers: { get: headerReader(response.headers) },
|
|
491
|
+
json: async () => {
|
|
492
|
+
const data = await readBody(response.data);
|
|
493
|
+
if (typeof data !== "string") return data;
|
|
494
|
+
return bodyIsRaw({ ...response, data }, declared) ? JSON.parse(data) : data;
|
|
495
|
+
},
|
|
496
|
+
text: async () => {
|
|
497
|
+
const data = await readBody(response.data);
|
|
498
|
+
return typeof data === "string" ? data : JSON.stringify(data);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function createAxiosHttpClient(instance, options = {}) {
|
|
503
|
+
const prefixes = typeof options.stripPrefix === "string" ? [options.stripPrefix] : options.stripPrefix ?? [];
|
|
504
|
+
const responseBody = options.responseBody ?? "auto";
|
|
505
|
+
return async (url, init) => {
|
|
506
|
+
try {
|
|
507
|
+
return adapt(
|
|
508
|
+
await instance.request({
|
|
509
|
+
url: stripped(url, prefixes),
|
|
510
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
511
|
+
headers: init?.headers,
|
|
512
|
+
data: init?.body
|
|
513
|
+
}),
|
|
514
|
+
responseBody
|
|
515
|
+
);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
const response = err?.response;
|
|
518
|
+
if (response && typeof response.status === "number") {
|
|
519
|
+
return adapt(response, responseBody);
|
|
520
|
+
}
|
|
521
|
+
throw isAxiosNoResponseError(err) ? markTransportFailure(err) : err;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/client/resources/define-resource.ts
|
|
527
|
+
function defineResource(key, ops, options = {}) {
|
|
528
|
+
return { key, ops, projectScoped: options.projectScoped ?? false };
|
|
529
|
+
}
|
|
530
|
+
function requireProjectKey(def, ctx) {
|
|
531
|
+
if (!def.projectScoped) return;
|
|
532
|
+
if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Resource "${def.key}" is project-scoped, but the context it is bound to names no project. Its requests would go out as \`?projectKey=\`, which the admin API answers for no project at all: the catalogue reads as empty and the first create fails validation instead. Name the project this admin administers \u2014 \`endpoints.projectKey\` for createSuperAdminApp(), \`context.projectKey\` for createResourceRegistry().`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
function bindResource(def, http, ctx) {
|
|
538
|
+
const readContext = typeof ctx === "function" ? ctx : () => ctx;
|
|
539
|
+
if (def.projectScoped) requireProjectKey(def, readContext());
|
|
540
|
+
const bound = {};
|
|
541
|
+
for (const [name, op] of Object.entries(def.ops)) {
|
|
542
|
+
bound[name] = async (...args) => {
|
|
543
|
+
const context = readContext();
|
|
544
|
+
requireProjectKey(def, context);
|
|
545
|
+
return op(http, context, ...args);
|
|
546
|
+
};
|
|
150
547
|
}
|
|
151
|
-
return
|
|
548
|
+
return bound;
|
|
152
549
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
550
|
+
|
|
551
|
+
// src/client/resources/resource-request.ts
|
|
552
|
+
async function requestJson(http, url, init = {}) {
|
|
553
|
+
const method = init.method ?? "GET";
|
|
554
|
+
const response = await http(url, {
|
|
555
|
+
method,
|
|
556
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
557
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
158
558
|
});
|
|
159
|
-
|
|
160
|
-
|
|
559
|
+
requireServerAnswer(
|
|
560
|
+
response.status,
|
|
561
|
+
method,
|
|
562
|
+
url,
|
|
563
|
+
(diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
|
|
564
|
+
);
|
|
565
|
+
if (response.status === 204) return null;
|
|
566
|
+
const body = await response.json().catch(() => null);
|
|
567
|
+
if (response.status >= 400) {
|
|
568
|
+
throw new AdminError({
|
|
569
|
+
status: response.status,
|
|
570
|
+
code: readErrorCode(body),
|
|
571
|
+
body,
|
|
572
|
+
// The third place this had to be read, and the second time it was
|
|
573
|
+
// read a narrower way: without `detail` a page migrating onto these
|
|
574
|
+
// resources loses "Plan already exists" and shows the generic
|
|
575
|
+
// status sentence instead. One decision, one function.
|
|
576
|
+
detail: readErrorDetail(body),
|
|
577
|
+
url,
|
|
578
|
+
method
|
|
579
|
+
});
|
|
161
580
|
}
|
|
162
|
-
return
|
|
581
|
+
return body;
|
|
582
|
+
}
|
|
583
|
+
async function requestJsonBody(http, url, what, init = {}) {
|
|
584
|
+
const body = await requestJson(http, url, init);
|
|
585
|
+
if (body === null || body === void 0) {
|
|
586
|
+
throw new AdminError({
|
|
587
|
+
status: 0,
|
|
588
|
+
url,
|
|
589
|
+
method: init.method ?? "GET",
|
|
590
|
+
// `what` is a diagnostic — "Create returned no body" — so it goes
|
|
591
|
+
// on `message`, not `detail`. As `detail` it out-ranked the
|
|
592
|
+
// localized wording and the operator read an English internal
|
|
593
|
+
// string instead of being told the change may already have landed.
|
|
594
|
+
message: what,
|
|
595
|
+
emptyResponse: true
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return body;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/client/resources/list-resource.ts
|
|
602
|
+
var LIST_FIRST_PAGE = 1;
|
|
603
|
+
var LIST_PAGE_SIZE_DEFAULT = 50;
|
|
604
|
+
var LIST_PAGE_SIZE_MAX = 200;
|
|
605
|
+
var LIST_PAGINATION_PARAMS = ["page", "pageSize"];
|
|
606
|
+
function isSentInQuery(value) {
|
|
607
|
+
return value !== void 0 && value !== null && value !== "";
|
|
608
|
+
}
|
|
609
|
+
function appendFilter(params, filter) {
|
|
610
|
+
for (const [key, value] of Object.entries(filter ?? {})) {
|
|
611
|
+
if (!isSentInQuery(value)) continue;
|
|
612
|
+
params.set(key, String(value));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function filterQueryString(filter) {
|
|
616
|
+
const params = new URLSearchParams();
|
|
617
|
+
appendFilter(params, filter);
|
|
618
|
+
const query = params.toString();
|
|
619
|
+
return query ? `?${query}` : "";
|
|
620
|
+
}
|
|
621
|
+
function listUrl(endpoint, query = {}) {
|
|
622
|
+
const params = new URLSearchParams();
|
|
623
|
+
params.set("page", String(query.page ?? LIST_FIRST_PAGE));
|
|
624
|
+
params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
|
|
625
|
+
appendFilter(params, query.filter);
|
|
626
|
+
return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
627
|
+
}
|
|
628
|
+
function readListPage(raw) {
|
|
629
|
+
if (Array.isArray(raw)) return { items: raw, total: raw.length };
|
|
630
|
+
if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
|
|
631
|
+
const body = raw;
|
|
632
|
+
const page = {
|
|
633
|
+
items: Array.isArray(body.items) ? body.items : []
|
|
634
|
+
};
|
|
635
|
+
if (typeof body.total === "number") page.total = body.total;
|
|
636
|
+
if (typeof body.page === "number") page.page = body.page;
|
|
637
|
+
if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
|
|
638
|
+
return page;
|
|
639
|
+
}
|
|
640
|
+
function clampListPage(page) {
|
|
641
|
+
return Math.max(LIST_FIRST_PAGE, Math.floor(page));
|
|
642
|
+
}
|
|
643
|
+
function clampListPageSize(size) {
|
|
644
|
+
return Math.min(LIST_PAGE_SIZE_MAX, Math.max(1, Math.floor(size)));
|
|
163
645
|
}
|
|
646
|
+
function defineListOp(endpoint) {
|
|
647
|
+
return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/client/resources/plans.resource.ts
|
|
651
|
+
function plansUrl(ctx) {
|
|
652
|
+
return `${ctx.apiBase}/catalog/plans`;
|
|
653
|
+
}
|
|
654
|
+
function versionsUrl(ctx) {
|
|
655
|
+
return `${ctx.apiBase}/catalog/plan-versions`;
|
|
656
|
+
}
|
|
657
|
+
function scoped(url, ctx) {
|
|
658
|
+
return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
|
|
659
|
+
}
|
|
660
|
+
var plansResource = defineResource(
|
|
661
|
+
"plans",
|
|
662
|
+
{
|
|
663
|
+
list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
|
|
664
|
+
tenantCounts: async (http, ctx) => await requestJson(
|
|
665
|
+
http,
|
|
666
|
+
scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
|
|
667
|
+
) ?? {},
|
|
668
|
+
/**
|
|
669
|
+
* The project comes from the context, not from the caller.
|
|
670
|
+
*
|
|
671
|
+
* The endpoint takes it in the body, and the composable this replaces
|
|
672
|
+
* made that the caller's job — so a page had to keep hold of the
|
|
673
|
+
* project it was administering just to create in it, and `list()` and
|
|
674
|
+
* `create()` read two different sources for one value. A stale one
|
|
675
|
+
* creates a plan outside the project the list is showing.
|
|
676
|
+
*/
|
|
677
|
+
create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
|
|
678
|
+
method: "POST",
|
|
679
|
+
body: { ...data, projectKey: ctx.projectKey }
|
|
680
|
+
}),
|
|
681
|
+
update: (http, ctx, planId, data) => requestJsonBody(
|
|
682
|
+
http,
|
|
683
|
+
`${plansUrl(ctx)}/${planId}`,
|
|
684
|
+
"Update returned no body",
|
|
685
|
+
{
|
|
686
|
+
method: "PATCH",
|
|
687
|
+
body: data
|
|
688
|
+
}
|
|
689
|
+
),
|
|
690
|
+
/** Marks the plan deleted. Fails while it still has versions. */
|
|
691
|
+
softDelete: async (http, ctx, planId) => {
|
|
692
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
|
|
693
|
+
},
|
|
694
|
+
/** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
|
|
695
|
+
hardDelete: async (http, ctx, planId) => {
|
|
696
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{ projectScoped: true }
|
|
700
|
+
);
|
|
701
|
+
var planVersionsResource = defineResource("planVersions", {
|
|
702
|
+
listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
|
|
703
|
+
createDraft: (http, ctx, planId, data) => requestJsonBody(
|
|
704
|
+
http,
|
|
705
|
+
`${plansUrl(ctx)}/${planId}/versions`,
|
|
706
|
+
"CreateDraft returned no body",
|
|
707
|
+
{ method: "POST", body: data }
|
|
708
|
+
),
|
|
709
|
+
updateDraft: (http, ctx, versionId, data) => requestJsonBody(
|
|
710
|
+
http,
|
|
711
|
+
`${versionsUrl(ctx)}/${versionId}`,
|
|
712
|
+
"UpdateDraft returned no body",
|
|
713
|
+
{ method: "PATCH", body: data }
|
|
714
|
+
),
|
|
715
|
+
publish: (http, ctx, versionId, options = {}) => requestJsonBody(
|
|
716
|
+
http,
|
|
717
|
+
`${versionsUrl(ctx)}/${versionId}/publish`,
|
|
718
|
+
"Publish returned no body",
|
|
719
|
+
{ method: "POST", body: options }
|
|
720
|
+
),
|
|
721
|
+
discardDraft: async (http, ctx, versionId) => {
|
|
722
|
+
await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
|
|
723
|
+
},
|
|
724
|
+
/**
|
|
725
|
+
* Ends a live version on `endsAt`, with no successor. Idempotent — a
|
|
726
|
+
* second call with another date overwrites the first.
|
|
727
|
+
*
|
|
728
|
+
* `endsAt` is the date itself, not a payload: the endpoint's body is
|
|
729
|
+
* `{ endsAt }` and wrapping it is this operation's job, not the caller's.
|
|
730
|
+
*/
|
|
731
|
+
terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
|
|
732
|
+
http,
|
|
733
|
+
`${versionsUrl(ctx)}/${versionId}/terminate`,
|
|
734
|
+
"Terminate returned no body",
|
|
735
|
+
{ method: "POST", body: { endsAt } }
|
|
736
|
+
)
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// src/client/resources/tenants.resource.ts
|
|
740
|
+
var tenantsResource = defineResource("tenants", {
|
|
741
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// src/client/resources/audit.resource.ts
|
|
745
|
+
var auditResource = defineResource("audit", {
|
|
746
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// src/client/resources/index.ts
|
|
750
|
+
var platformResources = {
|
|
751
|
+
plans: plansResource,
|
|
752
|
+
planVersions: planVersionsResource,
|
|
753
|
+
tenants: tenantsResource,
|
|
754
|
+
audit: auditResource
|
|
755
|
+
};
|
|
164
756
|
|
|
165
757
|
// src/client/boot-loader.ts
|
|
166
758
|
var BootLoadError = class extends Error {
|
|
@@ -168,6 +760,7 @@ var BootLoadError = class extends Error {
|
|
|
168
760
|
super(message);
|
|
169
761
|
this.status = status;
|
|
170
762
|
this.name = "BootLoadError";
|
|
763
|
+
markPlatformError(this);
|
|
171
764
|
}
|
|
172
765
|
status;
|
|
173
766
|
};
|
|
@@ -185,19 +778,27 @@ var BootLoader = class {
|
|
|
185
778
|
}
|
|
186
779
|
async load() {
|
|
187
780
|
const res = await this.http(this.endpoint);
|
|
781
|
+
requireServerAnswer(
|
|
782
|
+
res.status,
|
|
783
|
+
"GET",
|
|
784
|
+
this.endpoint,
|
|
785
|
+
(diagnostic) => new BootLoadError(res.status, diagnostic)
|
|
786
|
+
);
|
|
188
787
|
if (res.status !== 200) {
|
|
189
|
-
throw new BootLoadError(res.status, `Boot
|
|
788
|
+
throw new BootLoadError(res.status, `Boot endpoint responded with HTTP ${res.status}`);
|
|
190
789
|
}
|
|
191
790
|
return await res.json();
|
|
192
791
|
}
|
|
193
792
|
};
|
|
194
793
|
|
|
195
794
|
// src/client/manifest-loader.ts
|
|
795
|
+
var NOT_MODIFIED = 304;
|
|
196
796
|
var ManifestLoadError = class extends Error {
|
|
197
797
|
constructor(status, message) {
|
|
198
798
|
super(message);
|
|
199
799
|
this.status = status;
|
|
200
800
|
this.name = "ManifestLoadError";
|
|
801
|
+
markPlatformError(this);
|
|
201
802
|
}
|
|
202
803
|
status;
|
|
203
804
|
};
|
|
@@ -225,24 +826,48 @@ var ManifestLoader = class {
|
|
|
225
826
|
/**
|
|
226
827
|
* Loads the current manifest. On a cache hit (304) the cached
|
|
227
828
|
* body is returned — otherwise the fresh server body.
|
|
829
|
+
*
|
|
830
|
+
* A 304 with no usable cached body is not a failure to report but one to
|
|
831
|
+
* undo: the request only carried `If-None-Match` because this loader put
|
|
832
|
+
* the ETag there, and the cache it validated against is this loader's own.
|
|
833
|
+
* So it drops the pair and asks once more without the header, which is the
|
|
834
|
+
* same recovery a person would have had to trigger by hand — except that
|
|
835
|
+
* the person reading an admin screen has neither this loader nor a
|
|
836
|
+
* console. Exactly one retry: a 304 to a request that asked nothing
|
|
837
|
+
* conditional is a server fault, and no repetition fixes it.
|
|
228
838
|
*/
|
|
229
839
|
async load() {
|
|
230
840
|
const cachedEtag = this.storage.get(this.etagKey);
|
|
841
|
+
const res = await this.request(cachedEtag);
|
|
842
|
+
if (res.status !== NOT_MODIFIED) return this.acceptFresh(res);
|
|
843
|
+
const cached = this.readCachedBody();
|
|
844
|
+
if (cached) return cached.body;
|
|
845
|
+
if (cachedEtag) {
|
|
846
|
+
this.clearCache();
|
|
847
|
+
const fresh = await this.request(null);
|
|
848
|
+
if (fresh.status !== NOT_MODIFIED) return this.acceptFresh(fresh);
|
|
849
|
+
}
|
|
850
|
+
throw new ManifestLoadError(
|
|
851
|
+
NOT_MODIFIED,
|
|
852
|
+
"Manifest endpoint answered HTTP 304 to a request without If-None-Match"
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
/** Sends the manifest GET, conditional when an ETag is supplied. */
|
|
856
|
+
request(etag) {
|
|
231
857
|
const headers = {};
|
|
232
858
|
const token = this.getAuthToken?.();
|
|
233
859
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
860
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
861
|
+
return this.http(this.endpoint, { method: "GET", headers });
|
|
862
|
+
}
|
|
863
|
+
/** Takes a non-304 response as the current manifest and caches it. */
|
|
864
|
+
async acceptFresh(res) {
|
|
865
|
+
requireServerAnswer(
|
|
866
|
+
res.status,
|
|
867
|
+
"GET",
|
|
868
|
+
this.endpoint,
|
|
869
|
+
(diagnostic) => new ManifestLoadError(res.status, diagnostic)
|
|
870
|
+
);
|
|
246
871
|
if (res.status !== 200) {
|
|
247
872
|
throw new ManifestLoadError(
|
|
248
873
|
res.status,
|
|
@@ -577,7 +1202,7 @@ var ActionRegistry = class {
|
|
|
577
1202
|
// src/client/batch-column-fetcher.ts
|
|
578
1203
|
var BatchColumnDriftError = class extends Error {
|
|
579
1204
|
constructor(column, reason) {
|
|
580
|
-
super(`
|
|
1205
|
+
super(`Column "${column.key}": ${reason}`);
|
|
581
1206
|
this.column = column;
|
|
582
1207
|
this.name = "BatchColumnDriftError";
|
|
583
1208
|
}
|
|
@@ -620,7 +1245,7 @@ var BatchColumnFetcher = class {
|
|
|
620
1245
|
const res = await this.http(url, { method: "GET", headers });
|
|
621
1246
|
if (res.status !== 200) {
|
|
622
1247
|
throw new Error(
|
|
623
|
-
`
|
|
1248
|
+
`Column "${column.key}" \u2014 endpoint ${column.endpoint} responded with HTTP ${res.status}`
|
|
624
1249
|
);
|
|
625
1250
|
}
|
|
626
1251
|
const body = await res.json();
|
|
@@ -696,10 +1321,10 @@ function createAdminResourceClient(options) {
|
|
|
696
1321
|
tenantsEndpoint,
|
|
697
1322
|
loadTenantDetail: (slug) => getJson2(`${tenantsEndpoint}/${encodeURIComponent(slug)}`),
|
|
698
1323
|
loadUsers: (filter) => getJson2(
|
|
699
|
-
`${base}/users${
|
|
1324
|
+
`${base}/users${filterQueryString({ q: filter.q, tenant: filter.tenant })}`
|
|
700
1325
|
),
|
|
701
1326
|
loadAudit: (filter) => getJson2(
|
|
702
|
-
`${base}/audit${
|
|
1327
|
+
`${base}/audit${filterQueryString({
|
|
703
1328
|
actor: filter.actor,
|
|
704
1329
|
action: filter.action,
|
|
705
1330
|
entity: filter.entity,
|
|
@@ -709,7 +1334,7 @@ function createAdminResourceClient(options) {
|
|
|
709
1334
|
),
|
|
710
1335
|
loadSubscriptions: () => getJson2(`${base}/subscriptions`),
|
|
711
1336
|
loadPromos: (filter) => getJson2(
|
|
712
|
-
`${base}/promo-codes${
|
|
1337
|
+
`${base}/promo-codes${filterQueryString({
|
|
713
1338
|
search: filter.search,
|
|
714
1339
|
status: filter.status
|
|
715
1340
|
})}`
|
|
@@ -734,21 +1359,6 @@ function assertSuccess(status, method, url) {
|
|
|
734
1359
|
throw new Error(`${method} ${url} \u2192 HTTP ${status}`);
|
|
735
1360
|
}
|
|
736
1361
|
}
|
|
737
|
-
function queryString(params) {
|
|
738
|
-
const search = new URLSearchParams();
|
|
739
|
-
for (const [key, value2] of Object.entries(params)) {
|
|
740
|
-
if (value2 === void 0 || value2 === null || value2 === "") continue;
|
|
741
|
-
search.set(key, String(value2));
|
|
742
|
-
}
|
|
743
|
-
const value = search.toString();
|
|
744
|
-
return value ? `?${value}` : "";
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// src/client/i18n/format.ts
|
|
748
|
-
var import_types4 = require("@saasicat/types");
|
|
749
|
-
function formatMessage(template, params) {
|
|
750
|
-
return (0, import_types4.formatErrorMessage)(template, params);
|
|
751
|
-
}
|
|
752
1362
|
|
|
753
1363
|
// src/client/i18n/currency.ts
|
|
754
1364
|
var DEFAULT_CURRENCY = "EUR";
|
|
@@ -1447,9 +2057,6 @@ var discoveryMessages = defineMessages(
|
|
|
1447
2057
|
replacedBy: "ersetzt durch {key}",
|
|
1448
2058
|
replaces: "ersetzt: {keys}",
|
|
1449
2059
|
reapproveEmphasis: "erneut freigeben",
|
|
1450
|
-
errorDiscoveryHttp: "Discovery-Endpoint antwortete mit HTTP {status}",
|
|
1451
|
-
errorRescanHttp: "Discovery-Rescan antwortete mit HTTP {status}",
|
|
1452
|
-
errorCatalogHttp: "Catalog-Entries-API antwortete mit HTTP {status}",
|
|
1453
2060
|
capList: {
|
|
1454
2061
|
orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
|
|
1455
2062
|
removedFromCode: "aus Code entfernt"
|
|
@@ -1551,9 +2158,6 @@ var discoveryMessages = defineMessages(
|
|
|
1551
2158
|
replacedBy: "replaced by {key}",
|
|
1552
2159
|
replaces: "replaces: {keys}",
|
|
1553
2160
|
reapproveEmphasis: "approve again",
|
|
1554
|
-
errorDiscoveryHttp: "Discovery endpoint responded with HTTP {status}",
|
|
1555
|
-
errorRescanHttp: "Discovery rescan responded with HTTP {status}",
|
|
1556
|
-
errorCatalogHttp: "Catalog entries API responded with HTTP {status}",
|
|
1557
2161
|
capList: {
|
|
1558
2162
|
orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
|
|
1559
2163
|
removedFromCode: "removed from code"
|
|
@@ -1758,6 +2362,36 @@ var emailMessages = defineMessages(
|
|
|
1758
2362
|
}
|
|
1759
2363
|
);
|
|
1760
2364
|
|
|
2365
|
+
// src/client/i18n/messages/errors.ts
|
|
2366
|
+
var errorsMessages = defineMessages(
|
|
2367
|
+
{
|
|
2368
|
+
network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
|
|
2369
|
+
emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
|
|
2370
|
+
unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
|
|
2371
|
+
forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
|
|
2372
|
+
notFound: "Der angeforderte Eintrag existiert nicht mehr.",
|
|
2373
|
+
conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
|
|
2374
|
+
validation: "Die Eingaben wurden nicht akzeptiert.",
|
|
2375
|
+
rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
|
|
2376
|
+
server: "Der Server konnte die Anfrage nicht verarbeiten.",
|
|
2377
|
+
httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
|
|
2378
|
+
unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
network: "The request could not be completed. Please check your connection.",
|
|
2382
|
+
emptyResponse: "The server returned nothing. Please check whether the change was applied.",
|
|
2383
|
+
unauthorized: "Your session has expired. Please sign in again.",
|
|
2384
|
+
forbidden: "You do not have permission for this action.",
|
|
2385
|
+
notFound: "The requested entry no longer exists.",
|
|
2386
|
+
conflict: "The entry changed in the meantime. Please reload.",
|
|
2387
|
+
validation: "The input was not accepted.",
|
|
2388
|
+
rateLimited: "Too many requests. Please wait a moment and try again.",
|
|
2389
|
+
server: "The server could not process the request.",
|
|
2390
|
+
httpStatus: "The request failed (HTTP {status}).",
|
|
2391
|
+
unexpected: "Something went wrong. Please try again."
|
|
2392
|
+
}
|
|
2393
|
+
);
|
|
2394
|
+
|
|
1761
2395
|
// src/client/i18n/messages/marketing.ts
|
|
1762
2396
|
var marketingMessages = defineMessages(
|
|
1763
2397
|
{
|
|
@@ -1882,9 +2516,6 @@ var marketingMessages = defineMessages(
|
|
|
1882
2516
|
colorLabel: "Farbe",
|
|
1883
2517
|
delete: "Aktion l\xF6schen",
|
|
1884
2518
|
deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
|
|
1885
|
-
},
|
|
1886
|
-
errors: {
|
|
1887
|
-
projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
|
|
1888
2519
|
}
|
|
1889
2520
|
},
|
|
1890
2521
|
{
|
|
@@ -2009,9 +2640,6 @@ var marketingMessages = defineMessages(
|
|
|
2009
2640
|
colorLabel: "Color",
|
|
2010
2641
|
delete: "Delete promotion",
|
|
2011
2642
|
deleteConfirm: 'Really delete promotion "{label}"?'
|
|
2012
|
-
},
|
|
2013
|
-
errors: {
|
|
2014
|
-
projectionsApi: "MarketingProjections API responded with HTTP {status}"
|
|
2015
2643
|
}
|
|
2016
2644
|
}
|
|
2017
2645
|
);
|
|
@@ -3205,7 +3833,6 @@ var promosMessages = defineMessages(
|
|
|
3205
3833
|
{
|
|
3206
3834
|
title: "Promo-Codes",
|
|
3207
3835
|
createAction: "Promo-Code anlegen",
|
|
3208
|
-
apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
|
|
3209
3836
|
list: {
|
|
3210
3837
|
searchLabel: "Suche (Code)",
|
|
3211
3838
|
tileScheduled: "Geplant",
|
|
@@ -3303,7 +3930,6 @@ var promosMessages = defineMessages(
|
|
|
3303
3930
|
{
|
|
3304
3931
|
title: "Promo codes",
|
|
3305
3932
|
createAction: "Create promo code",
|
|
3306
|
-
apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
|
|
3307
3933
|
list: {
|
|
3308
3934
|
searchLabel: "Search (code)",
|
|
3309
3935
|
tileScheduled: "Scheduled",
|
|
@@ -3411,7 +4037,13 @@ var shellMessages = defineMessages(
|
|
|
3411
4037
|
subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
|
|
3412
4038
|
roleBadge: "SUPER ADMIN",
|
|
3413
4039
|
logout: "Abmelden",
|
|
3414
|
-
language: "Sprache"
|
|
4040
|
+
language: "Sprache",
|
|
4041
|
+
theme: {
|
|
4042
|
+
label: "Darstellung",
|
|
4043
|
+
light: "Hell",
|
|
4044
|
+
dark: "Dunkel",
|
|
4045
|
+
system: "System"
|
|
4046
|
+
}
|
|
3415
4047
|
},
|
|
3416
4048
|
drawer: {
|
|
3417
4049
|
docs: "Doku \xF6ffnen"
|
|
@@ -3480,7 +4112,13 @@ var shellMessages = defineMessages(
|
|
|
3480
4112
|
subtitle: "SuperAdmin \xB7 Platform administration",
|
|
3481
4113
|
roleBadge: "SUPER ADMIN",
|
|
3482
4114
|
logout: "Sign out",
|
|
3483
|
-
language: "Language"
|
|
4115
|
+
language: "Language",
|
|
4116
|
+
theme: {
|
|
4117
|
+
label: "Appearance",
|
|
4118
|
+
light: "Light",
|
|
4119
|
+
dark: "Dark",
|
|
4120
|
+
system: "System"
|
|
4121
|
+
}
|
|
3484
4122
|
},
|
|
3485
4123
|
drawer: {
|
|
3486
4124
|
docs: "Open documentation"
|
|
@@ -3724,6 +4362,7 @@ var usersMessages = defineMessages(
|
|
|
3724
4362
|
function catalogFor(locale) {
|
|
3725
4363
|
return {
|
|
3726
4364
|
common: commonMessages[locale],
|
|
4365
|
+
errors: errorsMessages[locale],
|
|
3727
4366
|
nav: navMessages[locale],
|
|
3728
4367
|
shell: shellMessages[locale],
|
|
3729
4368
|
dashboard: dashboardMessages[locale],
|
|
@@ -3912,6 +4551,7 @@ function identityChipStyle(accent) {
|
|
|
3912
4551
|
ADMIN_UI_VERSION,
|
|
3913
4552
|
ActionDefNotInManifestError,
|
|
3914
4553
|
ActionRegistry,
|
|
4554
|
+
AdminError,
|
|
3915
4555
|
BatchColumnDriftError,
|
|
3916
4556
|
BatchColumnFetcher,
|
|
3917
4557
|
BootLoadError,
|
|
@@ -3923,6 +4563,10 @@ function identityChipStyle(accent) {
|
|
|
3923
4563
|
IDENTITY_ACCENT_VALUES,
|
|
3924
4564
|
IDENTITY_NEUTRAL,
|
|
3925
4565
|
IDENTITY_NEUTRAL_VALUE,
|
|
4566
|
+
LIST_FIRST_PAGE,
|
|
4567
|
+
LIST_PAGE_SIZE_DEFAULT,
|
|
4568
|
+
LIST_PAGE_SIZE_MAX,
|
|
4569
|
+
LIST_PAGINATION_PARAMS,
|
|
3926
4570
|
ManifestLoadError,
|
|
3927
4571
|
ManifestLoader,
|
|
3928
4572
|
MissingHandlerError,
|
|
@@ -3930,32 +4574,63 @@ function identityChipStyle(accent) {
|
|
|
3930
4574
|
SA_LOCALES,
|
|
3931
4575
|
SA_LOCALE_LABELS,
|
|
3932
4576
|
SA_MESSAGES,
|
|
4577
|
+
adminErrorMessage,
|
|
4578
|
+
auditResource,
|
|
4579
|
+
bindResource,
|
|
3933
4580
|
buildRoutes,
|
|
3934
4581
|
buildSidebar,
|
|
4582
|
+
clampListPage,
|
|
4583
|
+
clampListPageSize,
|
|
3935
4584
|
countPlans,
|
|
3936
4585
|
createAdminResourceClient,
|
|
4586
|
+
createAxiosHttpClient,
|
|
4587
|
+
createFetchHttpClient,
|
|
3937
4588
|
createSaCatalog,
|
|
3938
4589
|
defaultHttpClient,
|
|
3939
4590
|
defaultKvStore,
|
|
3940
4591
|
defaultSectionOrder,
|
|
4592
|
+
defineListOp,
|
|
3941
4593
|
defineMessages,
|
|
4594
|
+
defineResource,
|
|
4595
|
+
filterQueryString,
|
|
3942
4596
|
formatCurrency,
|
|
3943
4597
|
formatMessage,
|
|
3944
4598
|
getJson,
|
|
3945
4599
|
identityAccentAt,
|
|
3946
4600
|
identityAccentFor,
|
|
3947
4601
|
identityChipStyle,
|
|
4602
|
+
isAdminError,
|
|
4603
|
+
isAxiosNoResponseError,
|
|
3948
4604
|
isCurrentlyValid,
|
|
4605
|
+
isEmptyResponse,
|
|
3949
4606
|
isExpired,
|
|
3950
4607
|
isFutureScheduled,
|
|
4608
|
+
isPlatformError,
|
|
3951
4609
|
isProductionBoot,
|
|
3952
4610
|
isSaBuiltinLocale,
|
|
4611
|
+
isSentInQuery,
|
|
4612
|
+
isTransportFailure,
|
|
4613
|
+
listUrl,
|
|
4614
|
+
markEmptyResponse,
|
|
4615
|
+
markPlatformError,
|
|
4616
|
+
markTransportFailure,
|
|
3953
4617
|
mergeMessages,
|
|
4618
|
+
planVersionsResource,
|
|
4619
|
+
plansResource,
|
|
4620
|
+
platformResources,
|
|
3954
4621
|
postJson,
|
|
4622
|
+
readErrorCode,
|
|
4623
|
+
readErrorDetail,
|
|
4624
|
+
readListPage,
|
|
4625
|
+
requestJson,
|
|
4626
|
+
requestJsonBody,
|
|
4627
|
+
requireServerAnswer,
|
|
3955
4628
|
resolveExtension,
|
|
3956
4629
|
resolveLoginBranding,
|
|
3957
4630
|
resolveMessages,
|
|
3958
4631
|
resolvePlans,
|
|
4632
|
+
tenantsResource,
|
|
4633
|
+
toAdminError,
|
|
3959
4634
|
todayIsoDate,
|
|
3960
4635
|
trimTrailingSlashes
|
|
3961
4636
|
});
|