@saasicat/ui-vue 0.25.0 → 0.26.1
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 +55 -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-COVQLDBZ.js} +759 -24
- package/dist/{chunk-TQQMKWN6.js → chunk-DT25QAU2.js} +119 -7
- package/dist/{chunk-D74FFCCP.js → chunk-O3Y3BYYS.js} +96 -178
- package/dist/client/index.cjs +770 -84
- package/dist/client/index.d.cts +429 -9
- package/dist/client/index.d.ts +429 -9
- package/dist/client/index.js +84 -12
- package/dist/index.cjs +1337 -295
- 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 +585 -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 +6 -4
- package/src/client/admin-error.ts +569 -0
- package/src/client/admin-resource-client.ts +9 -12
- package/src/client/attach-cause.ts +21 -0
- 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/nav-builder.ts +3 -1
- 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/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/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 +44 -7
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +7 -2
- 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/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,298 @@ 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/attach-cause.ts
|
|
115
|
+
function attachCause(error, cause) {
|
|
116
|
+
Object.defineProperty(error, "cause", {
|
|
117
|
+
value: cause,
|
|
118
|
+
configurable: true,
|
|
119
|
+
writable: true
|
|
120
|
+
});
|
|
121
|
+
return error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/client/i18n/format.ts
|
|
125
|
+
var import_types = require("@saasicat/types");
|
|
126
|
+
function formatMessage(template, params) {
|
|
127
|
+
return (0, import_types.formatErrorMessage)(template, params);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/client/admin-error.ts
|
|
131
|
+
var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
|
|
132
|
+
var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
|
|
133
|
+
function markPlatformError(error) {
|
|
134
|
+
Object.defineProperty(error, PLATFORM_ERROR, { value: true });
|
|
135
|
+
}
|
|
136
|
+
function isPlatformError(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && PLATFORM_ERROR in value;
|
|
138
|
+
}
|
|
139
|
+
var EMPTY_RESPONSE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/EmptyResponse");
|
|
140
|
+
function markEmptyResponse(error) {
|
|
141
|
+
Object.defineProperty(error, EMPTY_RESPONSE, { value: true });
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
function isEmptyResponse(value) {
|
|
145
|
+
return typeof value === "object" && value !== null && EMPTY_RESPONSE in value;
|
|
146
|
+
}
|
|
147
|
+
var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
|
|
148
|
+
function markTransportFailure(error) {
|
|
149
|
+
if (typeof error === "object" && error !== null) {
|
|
150
|
+
try {
|
|
151
|
+
Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
function isTransportFailure(value) {
|
|
158
|
+
return typeof value === "object" && value !== null && TRANSPORT_FAILURE in value;
|
|
159
|
+
}
|
|
160
|
+
function describe(init) {
|
|
161
|
+
const status = init.status ?? 0;
|
|
162
|
+
const where = [init.method, init.url].filter(Boolean).join(" ");
|
|
163
|
+
const what = status > 0 ? `HTTP ${status}` : "request failed";
|
|
164
|
+
const head = where ? `${where} \u2192 ${what}` : what;
|
|
165
|
+
const code = init.code ? ` (${init.code})` : "";
|
|
166
|
+
return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
|
|
167
|
+
}
|
|
168
|
+
var AdminError = class extends Error {
|
|
169
|
+
/** HTTP status. `0` means the request never produced one. */
|
|
170
|
+
status;
|
|
171
|
+
/** Machine-readable code from the response body, when it carried one. */
|
|
172
|
+
code;
|
|
173
|
+
/** Parsed response body, when there was one. */
|
|
174
|
+
body;
|
|
175
|
+
/** Requested URL, when known. */
|
|
176
|
+
url;
|
|
177
|
+
/** Request method, when known. */
|
|
178
|
+
method;
|
|
179
|
+
/**
|
|
180
|
+
* Text the failing side supplied — the only candidate for user-facing
|
|
181
|
+
* output. `undefined` when nothing was supplied.
|
|
182
|
+
*/
|
|
183
|
+
detail;
|
|
184
|
+
/** The request completed but the answer was unusable. See `AdminErrorInit`. */
|
|
185
|
+
emptyResponse;
|
|
186
|
+
/** The request never reached the server. See `AdminErrorInit`. */
|
|
187
|
+
transportFailure;
|
|
188
|
+
constructor(init = {}) {
|
|
189
|
+
super(init.message ?? describe(init));
|
|
190
|
+
if (init.cause !== void 0) attachCause(this, init.cause);
|
|
191
|
+
this.name = "AdminError";
|
|
192
|
+
this.status = init.status ?? 0;
|
|
193
|
+
this.code = init.code;
|
|
194
|
+
this.body = init.body;
|
|
195
|
+
this.url = init.url;
|
|
196
|
+
this.method = init.method;
|
|
197
|
+
this.detail = init.detail;
|
|
198
|
+
this.emptyResponse = init.emptyResponse ?? false;
|
|
199
|
+
this.transportFailure = init.transportFailure ?? false;
|
|
200
|
+
Object.defineProperty(this, ADMIN_ERROR, { value: true });
|
|
201
|
+
markPlatformError(this);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function isAdminError(err) {
|
|
205
|
+
return typeof err === "object" && err !== null && ADMIN_ERROR in err;
|
|
206
|
+
}
|
|
207
|
+
function asRecord(value) {
|
|
208
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
209
|
+
}
|
|
210
|
+
function asString(value) {
|
|
211
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
212
|
+
}
|
|
213
|
+
function isAxiosNoResponseError(value) {
|
|
214
|
+
const record = asRecord(value);
|
|
215
|
+
if (!record) return false;
|
|
216
|
+
return record.isAxiosError === true && record.request != null && record.response == null;
|
|
217
|
+
}
|
|
218
|
+
function readErrorDetail(body) {
|
|
219
|
+
const record = asRecord(body);
|
|
220
|
+
if (!record) return asString(body);
|
|
221
|
+
const message = record.message;
|
|
222
|
+
if (Array.isArray(message)) {
|
|
223
|
+
const parts = message.filter((part) => typeof part === "string");
|
|
224
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
225
|
+
}
|
|
226
|
+
return asString(message) ?? asString(record.error) ?? asString(record.reason);
|
|
227
|
+
}
|
|
228
|
+
function readErrorCode(body) {
|
|
229
|
+
return asString(asRecord(body)?.code);
|
|
230
|
+
}
|
|
231
|
+
function toAdminError(err) {
|
|
232
|
+
if (isAdminError(err)) return err;
|
|
233
|
+
const record = asRecord(err);
|
|
234
|
+
const response = asRecord(record?.response);
|
|
235
|
+
if (response && typeof response.status === "number") {
|
|
236
|
+
const config = asRecord(record?.config);
|
|
237
|
+
const body = response.data;
|
|
238
|
+
return new AdminError({
|
|
239
|
+
status: response.status,
|
|
240
|
+
code: readErrorCode(body),
|
|
241
|
+
body,
|
|
242
|
+
url: asString(config?.url),
|
|
243
|
+
method: asString(config?.method)?.toUpperCase(),
|
|
244
|
+
// Only what the response actually said. `err.message` here is
|
|
245
|
+
// axios's own generated line ("Request failed with status code
|
|
246
|
+
// 401") — English, untranslated, and about the transport rather
|
|
247
|
+
// than about what went wrong. Letting it through as `detail` would
|
|
248
|
+
// make `adminErrorMessage` return it in preference to the
|
|
249
|
+
// localized wording, which is the whole point of that wording.
|
|
250
|
+
detail: readErrorDetail(body),
|
|
251
|
+
cause: err
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (record && typeof record.status === "number") {
|
|
255
|
+
const ours = isPlatformError(err);
|
|
256
|
+
return new AdminError({
|
|
257
|
+
status: record.status,
|
|
258
|
+
code: readErrorCode(record.body) ?? asString(record.code),
|
|
259
|
+
body: record.body,
|
|
260
|
+
// Same reasoning as above: `BundlesApiError`'s message is
|
|
261
|
+
// "Bundles API responded with HTTP 403", a diagnostic. It stays on
|
|
262
|
+
// `message`, where logs read it.
|
|
263
|
+
detail: readErrorDetail(record.body) ?? (ours ? void 0 : asString(record.message)),
|
|
264
|
+
// Asked of the throw site, not of the class: only the site that
|
|
265
|
+
// read the response knows whether one arrived. `status === 0 &&
|
|
266
|
+
// ours` was the same question put to the wrong witness — the
|
|
267
|
+
// loaders raise branded errors from a status the client chose, so
|
|
268
|
+
// a client that resolves a transport failure as `0` turned a
|
|
269
|
+
// failed GET into "the change may have been applied".
|
|
270
|
+
emptyResponse: isEmptyResponse(err),
|
|
271
|
+
transportFailure: isTransportFailure(err),
|
|
272
|
+
message: asString(record.message),
|
|
273
|
+
cause: err
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (err instanceof Error) {
|
|
277
|
+
const transport = isTransportFailure(err) || isAxiosNoResponseError(err);
|
|
278
|
+
const saidSomethingReadable = !transport && !(err instanceof TypeError);
|
|
279
|
+
return new AdminError({
|
|
280
|
+
detail: saidSomethingReadable ? asString(err.message) : void 0,
|
|
281
|
+
transportFailure: transport,
|
|
282
|
+
message: err.message,
|
|
283
|
+
cause: err
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const text = asString(err) ?? asString(record?.message);
|
|
287
|
+
return new AdminError({
|
|
288
|
+
code: asString(record?.code),
|
|
289
|
+
detail: text,
|
|
290
|
+
message: text,
|
|
291
|
+
cause: err
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function statusKey(status) {
|
|
295
|
+
if (status === 400 || status === 422) return "validation";
|
|
296
|
+
if (status === 401) return "unauthorized";
|
|
297
|
+
if (status === 403) return "forbidden";
|
|
298
|
+
if (status === 404) return "notFound";
|
|
299
|
+
if (status === 409) return "conflict";
|
|
300
|
+
if (status === 429) return "rateLimited";
|
|
301
|
+
if (status >= 500) return "server";
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
function adminErrorMessage(err, msgs) {
|
|
305
|
+
const error = toAdminError(err);
|
|
306
|
+
if (error.detail) return error.detail;
|
|
307
|
+
if (error.emptyResponse) return msgs.emptyResponse;
|
|
308
|
+
if (error.transportFailure) return msgs.network;
|
|
309
|
+
if (error.status > 0) {
|
|
310
|
+
const key = statusKey(error.status);
|
|
311
|
+
return key ? msgs[key] : formatMessage(msgs.httpStatus, { status: error.status });
|
|
312
|
+
}
|
|
313
|
+
return msgs.unexpected;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/client/http-json.ts
|
|
317
|
+
function serverAnswered(status) {
|
|
318
|
+
return status > 0;
|
|
319
|
+
}
|
|
320
|
+
function requireServerAnswer(status, method, url, raise) {
|
|
321
|
+
if (serverAnswered(status)) return;
|
|
322
|
+
throw markTransportFailure(
|
|
323
|
+
raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
var HttpJsonError = AdminError;
|
|
327
|
+
async function readErrorBody(res) {
|
|
328
|
+
try {
|
|
329
|
+
return await res.json();
|
|
330
|
+
} catch {
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function failed(res, method, url) {
|
|
335
|
+
const body = await readErrorBody(res);
|
|
336
|
+
return new AdminError({
|
|
337
|
+
status: res.status,
|
|
338
|
+
// Read here rather than left to the status: `status: 0` reaches
|
|
339
|
+
// `adminErrorMessage` as "nothing is known about this failure", and
|
|
340
|
+
// this seam knows better — it held the response.
|
|
341
|
+
transportFailure: !serverAnswered(res.status),
|
|
342
|
+
code: readErrorCode(body),
|
|
343
|
+
body,
|
|
344
|
+
url,
|
|
345
|
+
method,
|
|
346
|
+
// Shared with `toAdminError` on purpose: a NestJS `ValidationPipe`
|
|
347
|
+
// rejection carries `message` as an array, and reading it here in a
|
|
348
|
+
// second, narrower way is how those constraints went missing.
|
|
349
|
+
detail: readErrorDetail(body)
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function trimTrailingSlashes(url) {
|
|
353
|
+
let end = url.length;
|
|
354
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
355
|
+
return url.slice(0, end);
|
|
356
|
+
}
|
|
357
|
+
async function getJson(http, url) {
|
|
358
|
+
const res = await http(url);
|
|
359
|
+
if (res.status < 200 || res.status >= 300) {
|
|
360
|
+
throw await failed(res, "GET", url);
|
|
361
|
+
}
|
|
362
|
+
return await res.json();
|
|
363
|
+
}
|
|
364
|
+
async function postJson(http, url, body) {
|
|
365
|
+
const res = await http(url, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: { "Content-Type": "application/json" },
|
|
368
|
+
body: JSON.stringify(body)
|
|
369
|
+
});
|
|
370
|
+
if (res.status < 200 || res.status >= 300) {
|
|
371
|
+
throw await failed(res, "POST", url);
|
|
372
|
+
}
|
|
373
|
+
return await res.json();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/client/http/fetch-http-client.ts
|
|
377
|
+
var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
|
|
378
|
+
function resolveUrl(url, baseUrl) {
|
|
379
|
+
if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
|
|
380
|
+
return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
381
|
+
}
|
|
382
|
+
function createFetchHttpClient(options = {}) {
|
|
383
|
+
return async (url, init) => {
|
|
384
|
+
const headers = new Headers(await options.headers?.());
|
|
385
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
386
|
+
for (const [name, value] of Object.entries(init?.headers ?? {})) {
|
|
387
|
+
headers.set(name, value);
|
|
388
|
+
}
|
|
389
|
+
if (init?.body !== void 0 && !headers.has("Content-Type")) {
|
|
390
|
+
headers.set("Content-Type", "application/json");
|
|
391
|
+
}
|
|
392
|
+
let response;
|
|
393
|
+
try {
|
|
394
|
+
response = await fetch(resolveUrl(url, options.baseUrl), {
|
|
395
|
+
method: init?.method ?? "GET",
|
|
396
|
+
headers,
|
|
397
|
+
body: init?.body
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
throw markTransportFailure(err);
|
|
401
|
+
}
|
|
402
|
+
return response;
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
78
406
|
// src/client/types.ts
|
|
79
407
|
var NOOP_KV_STORE = {
|
|
80
408
|
get: () => null,
|
|
@@ -116,51 +444,326 @@ function resolveLocalStorage() {
|
|
|
116
444
|
}
|
|
117
445
|
}
|
|
118
446
|
function defaultHttpClient() {
|
|
119
|
-
return (
|
|
447
|
+
return createFetchHttpClient();
|
|
120
448
|
}
|
|
121
449
|
|
|
122
|
-
// src/client/http-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
450
|
+
// src/client/http/axios-http-client.ts
|
|
451
|
+
function stripped(url, prefixes) {
|
|
452
|
+
for (const prefix of prefixes) {
|
|
453
|
+
const trimmed = trimTrailingSlashes(prefix);
|
|
454
|
+
if (!trimmed || !url.startsWith(trimmed)) continue;
|
|
455
|
+
const rest = url.slice(trimmed.length);
|
|
456
|
+
if (rest === "") return "/";
|
|
457
|
+
if (rest.startsWith("/")) return rest;
|
|
458
|
+
if (rest.startsWith("?") || rest.startsWith("#")) return `/${rest}`;
|
|
129
459
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
460
|
+
return url;
|
|
461
|
+
}
|
|
462
|
+
function headerReader(headers) {
|
|
463
|
+
const record = typeof headers === "object" && headers !== null ? headers : {};
|
|
464
|
+
const lowered = /* @__PURE__ */ new Map();
|
|
465
|
+
for (const [key, value] of Object.entries(record)) lowered.set(key.toLowerCase(), value);
|
|
466
|
+
return (name) => {
|
|
467
|
+
const value = lowered.get(name.toLowerCase());
|
|
468
|
+
return value == null ? null : String(value);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function bodyIsRaw(response, declared) {
|
|
472
|
+
if (declared !== "auto") return declared === "raw";
|
|
473
|
+
if (response.data === "") return true;
|
|
474
|
+
const config = response.config;
|
|
475
|
+
if (config?.transformResponse === null) return true;
|
|
476
|
+
if (Array.isArray(config?.transformResponse) && config.transformResponse.length === 0) {
|
|
477
|
+
return true;
|
|
139
478
|
}
|
|
479
|
+
const responseType = config?.responseType;
|
|
480
|
+
if (responseType === "json") return false;
|
|
481
|
+
if (typeof responseType === "string" && responseType !== "") return true;
|
|
482
|
+
const transitional = config?.transitional;
|
|
483
|
+
return typeof transitional === "object" && transitional !== null && transitional.forcedJSONParsing === false;
|
|
140
484
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
485
|
+
async function readBody(data) {
|
|
486
|
+
if (typeof data === "string" || data === null || data === void 0) return data;
|
|
487
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
488
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
|
|
489
|
+
const maybe = data;
|
|
490
|
+
if (typeof maybe.pipe === "function" || typeof maybe.getReader === "function") {
|
|
491
|
+
throw new TypeError(
|
|
492
|
+
"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."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (typeof maybe.text === "function") return maybe.text.call(data);
|
|
496
|
+
return data;
|
|
145
497
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
498
|
+
function adapt(response, declared) {
|
|
499
|
+
return {
|
|
500
|
+
status: response.status,
|
|
501
|
+
headers: { get: headerReader(response.headers) },
|
|
502
|
+
json: async () => {
|
|
503
|
+
const data = await readBody(response.data);
|
|
504
|
+
if (typeof data !== "string") return data;
|
|
505
|
+
return bodyIsRaw({ ...response, data }, declared) ? JSON.parse(data) : data;
|
|
506
|
+
},
|
|
507
|
+
text: async () => {
|
|
508
|
+
const data = await readBody(response.data);
|
|
509
|
+
return typeof data === "string" ? data : JSON.stringify(data);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function createAxiosHttpClient(instance, options = {}) {
|
|
514
|
+
const prefixes = typeof options.stripPrefix === "string" ? [options.stripPrefix] : options.stripPrefix ?? [];
|
|
515
|
+
const responseBody = options.responseBody ?? "auto";
|
|
516
|
+
return async (url, init) => {
|
|
517
|
+
try {
|
|
518
|
+
return adapt(
|
|
519
|
+
await instance.request({
|
|
520
|
+
url: stripped(url, prefixes),
|
|
521
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
522
|
+
headers: init?.headers,
|
|
523
|
+
data: init?.body
|
|
524
|
+
}),
|
|
525
|
+
responseBody
|
|
526
|
+
);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
const response = err?.response;
|
|
529
|
+
if (response && typeof response.status === "number") {
|
|
530
|
+
return adapt(response, responseBody);
|
|
531
|
+
}
|
|
532
|
+
throw isAxiosNoResponseError(err) ? markTransportFailure(err) : err;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/client/resources/define-resource.ts
|
|
538
|
+
function defineResource(key, ops, options = {}) {
|
|
539
|
+
return { key, ops, projectScoped: options.projectScoped ?? false };
|
|
540
|
+
}
|
|
541
|
+
function requireProjectKey(def, ctx) {
|
|
542
|
+
if (!def.projectScoped) return;
|
|
543
|
+
if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
|
|
544
|
+
throw new Error(
|
|
545
|
+
`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().`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
function bindResource(def, http, ctx) {
|
|
549
|
+
const readContext = typeof ctx === "function" ? ctx : () => ctx;
|
|
550
|
+
if (def.projectScoped) requireProjectKey(def, readContext());
|
|
551
|
+
const bound = {};
|
|
552
|
+
for (const [name, op] of Object.entries(def.ops)) {
|
|
553
|
+
bound[name] = async (...args) => {
|
|
554
|
+
const context = readContext();
|
|
555
|
+
requireProjectKey(def, context);
|
|
556
|
+
return op(http, context, ...args);
|
|
557
|
+
};
|
|
150
558
|
}
|
|
151
|
-
return
|
|
559
|
+
return bound;
|
|
152
560
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
561
|
+
|
|
562
|
+
// src/client/resources/resource-request.ts
|
|
563
|
+
async function requestJson(http, url, init = {}) {
|
|
564
|
+
const method = init.method ?? "GET";
|
|
565
|
+
const response = await http(url, {
|
|
566
|
+
method,
|
|
567
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
568
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
158
569
|
});
|
|
159
|
-
|
|
160
|
-
|
|
570
|
+
requireServerAnswer(
|
|
571
|
+
response.status,
|
|
572
|
+
method,
|
|
573
|
+
url,
|
|
574
|
+
(diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
|
|
575
|
+
);
|
|
576
|
+
if (response.status === 204) return null;
|
|
577
|
+
const body = await response.json().catch(() => null);
|
|
578
|
+
if (response.status >= 400) {
|
|
579
|
+
throw new AdminError({
|
|
580
|
+
status: response.status,
|
|
581
|
+
code: readErrorCode(body),
|
|
582
|
+
body,
|
|
583
|
+
// The third place this had to be read, and the second time it was
|
|
584
|
+
// read a narrower way: without `detail` a page migrating onto these
|
|
585
|
+
// resources loses "Plan already exists" and shows the generic
|
|
586
|
+
// status sentence instead. One decision, one function.
|
|
587
|
+
detail: readErrorDetail(body),
|
|
588
|
+
url,
|
|
589
|
+
method
|
|
590
|
+
});
|
|
161
591
|
}
|
|
162
|
-
return
|
|
592
|
+
return body;
|
|
593
|
+
}
|
|
594
|
+
async function requestJsonBody(http, url, what, init = {}) {
|
|
595
|
+
const body = await requestJson(http, url, init);
|
|
596
|
+
if (body === null || body === void 0) {
|
|
597
|
+
throw new AdminError({
|
|
598
|
+
status: 0,
|
|
599
|
+
url,
|
|
600
|
+
method: init.method ?? "GET",
|
|
601
|
+
// `what` is a diagnostic — "Create returned no body" — so it goes
|
|
602
|
+
// on `message`, not `detail`. As `detail` it out-ranked the
|
|
603
|
+
// localized wording and the operator read an English internal
|
|
604
|
+
// string instead of being told the change may already have landed.
|
|
605
|
+
message: what,
|
|
606
|
+
emptyResponse: true
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
return body;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/client/resources/list-resource.ts
|
|
613
|
+
var LIST_FIRST_PAGE = 1;
|
|
614
|
+
var LIST_PAGE_SIZE_DEFAULT = 50;
|
|
615
|
+
var LIST_PAGE_SIZE_MAX = 200;
|
|
616
|
+
var LIST_PAGINATION_PARAMS = ["page", "pageSize"];
|
|
617
|
+
function isSentInQuery(value) {
|
|
618
|
+
return value !== void 0 && value !== null && value !== "";
|
|
619
|
+
}
|
|
620
|
+
function appendFilter(params, filter) {
|
|
621
|
+
for (const [key, value] of Object.entries(filter ?? {})) {
|
|
622
|
+
if (!isSentInQuery(value)) continue;
|
|
623
|
+
params.set(key, String(value));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function filterQueryString(filter) {
|
|
627
|
+
const params = new URLSearchParams();
|
|
628
|
+
appendFilter(params, filter);
|
|
629
|
+
const query = params.toString();
|
|
630
|
+
return query ? `?${query}` : "";
|
|
631
|
+
}
|
|
632
|
+
function listUrl(endpoint, query = {}) {
|
|
633
|
+
const params = new URLSearchParams();
|
|
634
|
+
params.set("page", String(query.page ?? LIST_FIRST_PAGE));
|
|
635
|
+
params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
|
|
636
|
+
appendFilter(params, query.filter);
|
|
637
|
+
return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
638
|
+
}
|
|
639
|
+
function readListPage(raw) {
|
|
640
|
+
if (Array.isArray(raw)) return { items: raw, total: raw.length };
|
|
641
|
+
if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
|
|
642
|
+
const body = raw;
|
|
643
|
+
const page = {
|
|
644
|
+
items: Array.isArray(body.items) ? body.items : []
|
|
645
|
+
};
|
|
646
|
+
if (typeof body.total === "number") page.total = body.total;
|
|
647
|
+
if (typeof body.page === "number") page.page = body.page;
|
|
648
|
+
if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
|
|
649
|
+
return page;
|
|
650
|
+
}
|
|
651
|
+
function clampListPage(page) {
|
|
652
|
+
return Math.max(LIST_FIRST_PAGE, Math.floor(page));
|
|
163
653
|
}
|
|
654
|
+
function clampListPageSize(size) {
|
|
655
|
+
return Math.min(LIST_PAGE_SIZE_MAX, Math.max(1, Math.floor(size)));
|
|
656
|
+
}
|
|
657
|
+
function defineListOp(endpoint) {
|
|
658
|
+
return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/client/resources/plans.resource.ts
|
|
662
|
+
function plansUrl(ctx) {
|
|
663
|
+
return `${ctx.apiBase}/catalog/plans`;
|
|
664
|
+
}
|
|
665
|
+
function versionsUrl(ctx) {
|
|
666
|
+
return `${ctx.apiBase}/catalog/plan-versions`;
|
|
667
|
+
}
|
|
668
|
+
function scoped(url, ctx) {
|
|
669
|
+
return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
|
|
670
|
+
}
|
|
671
|
+
var plansResource = defineResource(
|
|
672
|
+
"plans",
|
|
673
|
+
{
|
|
674
|
+
list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
|
|
675
|
+
tenantCounts: async (http, ctx) => await requestJson(
|
|
676
|
+
http,
|
|
677
|
+
scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
|
|
678
|
+
) ?? {},
|
|
679
|
+
/**
|
|
680
|
+
* The project comes from the context, not from the caller.
|
|
681
|
+
*
|
|
682
|
+
* The endpoint takes it in the body, and the composable this replaces
|
|
683
|
+
* made that the caller's job — so a page had to keep hold of the
|
|
684
|
+
* project it was administering just to create in it, and `list()` and
|
|
685
|
+
* `create()` read two different sources for one value. A stale one
|
|
686
|
+
* creates a plan outside the project the list is showing.
|
|
687
|
+
*/
|
|
688
|
+
create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
|
|
689
|
+
method: "POST",
|
|
690
|
+
body: { ...data, projectKey: ctx.projectKey }
|
|
691
|
+
}),
|
|
692
|
+
update: (http, ctx, planId, data) => requestJsonBody(
|
|
693
|
+
http,
|
|
694
|
+
`${plansUrl(ctx)}/${planId}`,
|
|
695
|
+
"Update returned no body",
|
|
696
|
+
{
|
|
697
|
+
method: "PATCH",
|
|
698
|
+
body: data
|
|
699
|
+
}
|
|
700
|
+
),
|
|
701
|
+
/** Marks the plan deleted. Fails while it still has versions. */
|
|
702
|
+
softDelete: async (http, ctx, planId) => {
|
|
703
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
|
|
704
|
+
},
|
|
705
|
+
/** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
|
|
706
|
+
hardDelete: async (http, ctx, planId) => {
|
|
707
|
+
await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
{ projectScoped: true }
|
|
711
|
+
);
|
|
712
|
+
var planVersionsResource = defineResource("planVersions", {
|
|
713
|
+
listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
|
|
714
|
+
createDraft: (http, ctx, planId, data) => requestJsonBody(
|
|
715
|
+
http,
|
|
716
|
+
`${plansUrl(ctx)}/${planId}/versions`,
|
|
717
|
+
"CreateDraft returned no body",
|
|
718
|
+
{ method: "POST", body: data }
|
|
719
|
+
),
|
|
720
|
+
updateDraft: (http, ctx, versionId, data) => requestJsonBody(
|
|
721
|
+
http,
|
|
722
|
+
`${versionsUrl(ctx)}/${versionId}`,
|
|
723
|
+
"UpdateDraft returned no body",
|
|
724
|
+
{ method: "PATCH", body: data }
|
|
725
|
+
),
|
|
726
|
+
publish: (http, ctx, versionId, options = {}) => requestJsonBody(
|
|
727
|
+
http,
|
|
728
|
+
`${versionsUrl(ctx)}/${versionId}/publish`,
|
|
729
|
+
"Publish returned no body",
|
|
730
|
+
{ method: "POST", body: options }
|
|
731
|
+
),
|
|
732
|
+
discardDraft: async (http, ctx, versionId) => {
|
|
733
|
+
await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
|
|
734
|
+
},
|
|
735
|
+
/**
|
|
736
|
+
* Ends a live version on `endsAt`, with no successor. Idempotent — a
|
|
737
|
+
* second call with another date overwrites the first.
|
|
738
|
+
*
|
|
739
|
+
* `endsAt` is the date itself, not a payload: the endpoint's body is
|
|
740
|
+
* `{ endsAt }` and wrapping it is this operation's job, not the caller's.
|
|
741
|
+
*/
|
|
742
|
+
terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
|
|
743
|
+
http,
|
|
744
|
+
`${versionsUrl(ctx)}/${versionId}/terminate`,
|
|
745
|
+
"Terminate returned no body",
|
|
746
|
+
{ method: "POST", body: { endsAt } }
|
|
747
|
+
)
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// src/client/resources/tenants.resource.ts
|
|
751
|
+
var tenantsResource = defineResource("tenants", {
|
|
752
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// src/client/resources/audit.resource.ts
|
|
756
|
+
var auditResource = defineResource("audit", {
|
|
757
|
+
list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// src/client/resources/index.ts
|
|
761
|
+
var platformResources = {
|
|
762
|
+
plans: plansResource,
|
|
763
|
+
planVersions: planVersionsResource,
|
|
764
|
+
tenants: tenantsResource,
|
|
765
|
+
audit: auditResource
|
|
766
|
+
};
|
|
164
767
|
|
|
165
768
|
// src/client/boot-loader.ts
|
|
166
769
|
var BootLoadError = class extends Error {
|
|
@@ -168,6 +771,7 @@ var BootLoadError = class extends Error {
|
|
|
168
771
|
super(message);
|
|
169
772
|
this.status = status;
|
|
170
773
|
this.name = "BootLoadError";
|
|
774
|
+
markPlatformError(this);
|
|
171
775
|
}
|
|
172
776
|
status;
|
|
173
777
|
};
|
|
@@ -185,19 +789,27 @@ var BootLoader = class {
|
|
|
185
789
|
}
|
|
186
790
|
async load() {
|
|
187
791
|
const res = await this.http(this.endpoint);
|
|
792
|
+
requireServerAnswer(
|
|
793
|
+
res.status,
|
|
794
|
+
"GET",
|
|
795
|
+
this.endpoint,
|
|
796
|
+
(diagnostic) => new BootLoadError(res.status, diagnostic)
|
|
797
|
+
);
|
|
188
798
|
if (res.status !== 200) {
|
|
189
|
-
throw new BootLoadError(res.status, `Boot
|
|
799
|
+
throw new BootLoadError(res.status, `Boot endpoint responded with HTTP ${res.status}`);
|
|
190
800
|
}
|
|
191
801
|
return await res.json();
|
|
192
802
|
}
|
|
193
803
|
};
|
|
194
804
|
|
|
195
805
|
// src/client/manifest-loader.ts
|
|
806
|
+
var NOT_MODIFIED = 304;
|
|
196
807
|
var ManifestLoadError = class extends Error {
|
|
197
808
|
constructor(status, message) {
|
|
198
809
|
super(message);
|
|
199
810
|
this.status = status;
|
|
200
811
|
this.name = "ManifestLoadError";
|
|
812
|
+
markPlatformError(this);
|
|
201
813
|
}
|
|
202
814
|
status;
|
|
203
815
|
};
|
|
@@ -225,24 +837,48 @@ var ManifestLoader = class {
|
|
|
225
837
|
/**
|
|
226
838
|
* Loads the current manifest. On a cache hit (304) the cached
|
|
227
839
|
* body is returned — otherwise the fresh server body.
|
|
840
|
+
*
|
|
841
|
+
* A 304 with no usable cached body is not a failure to report but one to
|
|
842
|
+
* undo: the request only carried `If-None-Match` because this loader put
|
|
843
|
+
* the ETag there, and the cache it validated against is this loader's own.
|
|
844
|
+
* So it drops the pair and asks once more without the header, which is the
|
|
845
|
+
* same recovery a person would have had to trigger by hand — except that
|
|
846
|
+
* the person reading an admin screen has neither this loader nor a
|
|
847
|
+
* console. Exactly one retry: a 304 to a request that asked nothing
|
|
848
|
+
* conditional is a server fault, and no repetition fixes it.
|
|
228
849
|
*/
|
|
229
850
|
async load() {
|
|
230
851
|
const cachedEtag = this.storage.get(this.etagKey);
|
|
852
|
+
const res = await this.request(cachedEtag);
|
|
853
|
+
if (res.status !== NOT_MODIFIED) return this.acceptFresh(res);
|
|
854
|
+
const cached = this.readCachedBody();
|
|
855
|
+
if (cached) return cached.body;
|
|
856
|
+
if (cachedEtag) {
|
|
857
|
+
this.clearCache();
|
|
858
|
+
const fresh = await this.request(null);
|
|
859
|
+
if (fresh.status !== NOT_MODIFIED) return this.acceptFresh(fresh);
|
|
860
|
+
}
|
|
861
|
+
throw new ManifestLoadError(
|
|
862
|
+
NOT_MODIFIED,
|
|
863
|
+
"Manifest endpoint answered HTTP 304 to a request without If-None-Match"
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
/** Sends the manifest GET, conditional when an ETag is supplied. */
|
|
867
|
+
request(etag) {
|
|
231
868
|
const headers = {};
|
|
232
869
|
const token = this.getAuthToken?.();
|
|
233
870
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
871
|
+
if (etag) headers["If-None-Match"] = etag;
|
|
872
|
+
return this.http(this.endpoint, { method: "GET", headers });
|
|
873
|
+
}
|
|
874
|
+
/** Takes a non-304 response as the current manifest and caches it. */
|
|
875
|
+
async acceptFresh(res) {
|
|
876
|
+
requireServerAnswer(
|
|
877
|
+
res.status,
|
|
878
|
+
"GET",
|
|
879
|
+
this.endpoint,
|
|
880
|
+
(diagnostic) => new ManifestLoadError(res.status, diagnostic)
|
|
881
|
+
);
|
|
246
882
|
if (res.status !== 200) {
|
|
247
883
|
throw new ManifestLoadError(
|
|
248
884
|
res.status,
|
|
@@ -378,7 +1014,7 @@ var DEFAULT_STANDARD_PAGE_ROUTES = {
|
|
|
378
1014
|
platformEmailHistory: "/admin/platform-email-history"
|
|
379
1015
|
};
|
|
380
1016
|
function isSupportedStandardPageKey(key) {
|
|
381
|
-
return Object.
|
|
1017
|
+
return Object.prototype.hasOwnProperty.call(DEFAULT_STANDARD_PAGE_ROUTES, key);
|
|
382
1018
|
}
|
|
383
1019
|
var DEFAULT_ICONS = {
|
|
384
1020
|
dashboard: "dashboard",
|
|
@@ -577,7 +1213,7 @@ var ActionRegistry = class {
|
|
|
577
1213
|
// src/client/batch-column-fetcher.ts
|
|
578
1214
|
var BatchColumnDriftError = class extends Error {
|
|
579
1215
|
constructor(column, reason) {
|
|
580
|
-
super(`
|
|
1216
|
+
super(`Column "${column.key}": ${reason}`);
|
|
581
1217
|
this.column = column;
|
|
582
1218
|
this.name = "BatchColumnDriftError";
|
|
583
1219
|
}
|
|
@@ -620,7 +1256,7 @@ var BatchColumnFetcher = class {
|
|
|
620
1256
|
const res = await this.http(url, { method: "GET", headers });
|
|
621
1257
|
if (res.status !== 200) {
|
|
622
1258
|
throw new Error(
|
|
623
|
-
`
|
|
1259
|
+
`Column "${column.key}" \u2014 endpoint ${column.endpoint} responded with HTTP ${res.status}`
|
|
624
1260
|
);
|
|
625
1261
|
}
|
|
626
1262
|
const body = await res.json();
|
|
@@ -696,10 +1332,10 @@ function createAdminResourceClient(options) {
|
|
|
696
1332
|
tenantsEndpoint,
|
|
697
1333
|
loadTenantDetail: (slug) => getJson2(`${tenantsEndpoint}/${encodeURIComponent(slug)}`),
|
|
698
1334
|
loadUsers: (filter) => getJson2(
|
|
699
|
-
`${base}/users${
|
|
1335
|
+
`${base}/users${filterQueryString({ q: filter.q, tenant: filter.tenant })}`
|
|
700
1336
|
),
|
|
701
1337
|
loadAudit: (filter) => getJson2(
|
|
702
|
-
`${base}/audit${
|
|
1338
|
+
`${base}/audit${filterQueryString({
|
|
703
1339
|
actor: filter.actor,
|
|
704
1340
|
action: filter.action,
|
|
705
1341
|
entity: filter.entity,
|
|
@@ -709,7 +1345,7 @@ function createAdminResourceClient(options) {
|
|
|
709
1345
|
),
|
|
710
1346
|
loadSubscriptions: () => getJson2(`${base}/subscriptions`),
|
|
711
1347
|
loadPromos: (filter) => getJson2(
|
|
712
|
-
`${base}/promo-codes${
|
|
1348
|
+
`${base}/promo-codes${filterQueryString({
|
|
713
1349
|
search: filter.search,
|
|
714
1350
|
status: filter.status
|
|
715
1351
|
})}`
|
|
@@ -734,21 +1370,6 @@ function assertSuccess(status, method, url) {
|
|
|
734
1370
|
throw new Error(`${method} ${url} \u2192 HTTP ${status}`);
|
|
735
1371
|
}
|
|
736
1372
|
}
|
|
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
1373
|
|
|
753
1374
|
// src/client/i18n/currency.ts
|
|
754
1375
|
var DEFAULT_CURRENCY = "EUR";
|
|
@@ -1447,9 +2068,6 @@ var discoveryMessages = defineMessages(
|
|
|
1447
2068
|
replacedBy: "ersetzt durch {key}",
|
|
1448
2069
|
replaces: "ersetzt: {keys}",
|
|
1449
2070
|
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
2071
|
capList: {
|
|
1454
2072
|
orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
|
|
1455
2073
|
removedFromCode: "aus Code entfernt"
|
|
@@ -1551,9 +2169,6 @@ var discoveryMessages = defineMessages(
|
|
|
1551
2169
|
replacedBy: "replaced by {key}",
|
|
1552
2170
|
replaces: "replaces: {keys}",
|
|
1553
2171
|
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
2172
|
capList: {
|
|
1558
2173
|
orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
|
|
1559
2174
|
removedFromCode: "removed from code"
|
|
@@ -1758,6 +2373,36 @@ var emailMessages = defineMessages(
|
|
|
1758
2373
|
}
|
|
1759
2374
|
);
|
|
1760
2375
|
|
|
2376
|
+
// src/client/i18n/messages/errors.ts
|
|
2377
|
+
var errorsMessages = defineMessages(
|
|
2378
|
+
{
|
|
2379
|
+
network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
|
|
2380
|
+
emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
|
|
2381
|
+
unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
|
|
2382
|
+
forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
|
|
2383
|
+
notFound: "Der angeforderte Eintrag existiert nicht mehr.",
|
|
2384
|
+
conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
|
|
2385
|
+
validation: "Die Eingaben wurden nicht akzeptiert.",
|
|
2386
|
+
rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
|
|
2387
|
+
server: "Der Server konnte die Anfrage nicht verarbeiten.",
|
|
2388
|
+
httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
|
|
2389
|
+
unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
network: "The request could not be completed. Please check your connection.",
|
|
2393
|
+
emptyResponse: "The server returned nothing. Please check whether the change was applied.",
|
|
2394
|
+
unauthorized: "Your session has expired. Please sign in again.",
|
|
2395
|
+
forbidden: "You do not have permission for this action.",
|
|
2396
|
+
notFound: "The requested entry no longer exists.",
|
|
2397
|
+
conflict: "The entry changed in the meantime. Please reload.",
|
|
2398
|
+
validation: "The input was not accepted.",
|
|
2399
|
+
rateLimited: "Too many requests. Please wait a moment and try again.",
|
|
2400
|
+
server: "The server could not process the request.",
|
|
2401
|
+
httpStatus: "The request failed (HTTP {status}).",
|
|
2402
|
+
unexpected: "Something went wrong. Please try again."
|
|
2403
|
+
}
|
|
2404
|
+
);
|
|
2405
|
+
|
|
1761
2406
|
// src/client/i18n/messages/marketing.ts
|
|
1762
2407
|
var marketingMessages = defineMessages(
|
|
1763
2408
|
{
|
|
@@ -1882,9 +2527,6 @@ var marketingMessages = defineMessages(
|
|
|
1882
2527
|
colorLabel: "Farbe",
|
|
1883
2528
|
delete: "Aktion l\xF6schen",
|
|
1884
2529
|
deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
|
|
1885
|
-
},
|
|
1886
|
-
errors: {
|
|
1887
|
-
projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
|
|
1888
2530
|
}
|
|
1889
2531
|
},
|
|
1890
2532
|
{
|
|
@@ -2009,9 +2651,6 @@ var marketingMessages = defineMessages(
|
|
|
2009
2651
|
colorLabel: "Color",
|
|
2010
2652
|
delete: "Delete promotion",
|
|
2011
2653
|
deleteConfirm: 'Really delete promotion "{label}"?'
|
|
2012
|
-
},
|
|
2013
|
-
errors: {
|
|
2014
|
-
projectionsApi: "MarketingProjections API responded with HTTP {status}"
|
|
2015
2654
|
}
|
|
2016
2655
|
}
|
|
2017
2656
|
);
|
|
@@ -3205,7 +3844,6 @@ var promosMessages = defineMessages(
|
|
|
3205
3844
|
{
|
|
3206
3845
|
title: "Promo-Codes",
|
|
3207
3846
|
createAction: "Promo-Code anlegen",
|
|
3208
|
-
apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
|
|
3209
3847
|
list: {
|
|
3210
3848
|
searchLabel: "Suche (Code)",
|
|
3211
3849
|
tileScheduled: "Geplant",
|
|
@@ -3303,7 +3941,6 @@ var promosMessages = defineMessages(
|
|
|
3303
3941
|
{
|
|
3304
3942
|
title: "Promo codes",
|
|
3305
3943
|
createAction: "Create promo code",
|
|
3306
|
-
apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
|
|
3307
3944
|
list: {
|
|
3308
3945
|
searchLabel: "Search (code)",
|
|
3309
3946
|
tileScheduled: "Scheduled",
|
|
@@ -3411,7 +4048,13 @@ var shellMessages = defineMessages(
|
|
|
3411
4048
|
subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
|
|
3412
4049
|
roleBadge: "SUPER ADMIN",
|
|
3413
4050
|
logout: "Abmelden",
|
|
3414
|
-
language: "Sprache"
|
|
4051
|
+
language: "Sprache",
|
|
4052
|
+
theme: {
|
|
4053
|
+
label: "Darstellung",
|
|
4054
|
+
light: "Hell",
|
|
4055
|
+
dark: "Dunkel",
|
|
4056
|
+
system: "System"
|
|
4057
|
+
}
|
|
3415
4058
|
},
|
|
3416
4059
|
drawer: {
|
|
3417
4060
|
docs: "Doku \xF6ffnen"
|
|
@@ -3480,7 +4123,13 @@ var shellMessages = defineMessages(
|
|
|
3480
4123
|
subtitle: "SuperAdmin \xB7 Platform administration",
|
|
3481
4124
|
roleBadge: "SUPER ADMIN",
|
|
3482
4125
|
logout: "Sign out",
|
|
3483
|
-
language: "Language"
|
|
4126
|
+
language: "Language",
|
|
4127
|
+
theme: {
|
|
4128
|
+
label: "Appearance",
|
|
4129
|
+
light: "Light",
|
|
4130
|
+
dark: "Dark",
|
|
4131
|
+
system: "System"
|
|
4132
|
+
}
|
|
3484
4133
|
},
|
|
3485
4134
|
drawer: {
|
|
3486
4135
|
docs: "Open documentation"
|
|
@@ -3724,6 +4373,7 @@ var usersMessages = defineMessages(
|
|
|
3724
4373
|
function catalogFor(locale) {
|
|
3725
4374
|
return {
|
|
3726
4375
|
common: commonMessages[locale],
|
|
4376
|
+
errors: errorsMessages[locale],
|
|
3727
4377
|
nav: navMessages[locale],
|
|
3728
4378
|
shell: shellMessages[locale],
|
|
3729
4379
|
dashboard: dashboardMessages[locale],
|
|
@@ -3912,6 +4562,7 @@ function identityChipStyle(accent) {
|
|
|
3912
4562
|
ADMIN_UI_VERSION,
|
|
3913
4563
|
ActionDefNotInManifestError,
|
|
3914
4564
|
ActionRegistry,
|
|
4565
|
+
AdminError,
|
|
3915
4566
|
BatchColumnDriftError,
|
|
3916
4567
|
BatchColumnFetcher,
|
|
3917
4568
|
BootLoadError,
|
|
@@ -3923,6 +4574,10 @@ function identityChipStyle(accent) {
|
|
|
3923
4574
|
IDENTITY_ACCENT_VALUES,
|
|
3924
4575
|
IDENTITY_NEUTRAL,
|
|
3925
4576
|
IDENTITY_NEUTRAL_VALUE,
|
|
4577
|
+
LIST_FIRST_PAGE,
|
|
4578
|
+
LIST_PAGE_SIZE_DEFAULT,
|
|
4579
|
+
LIST_PAGE_SIZE_MAX,
|
|
4580
|
+
LIST_PAGINATION_PARAMS,
|
|
3926
4581
|
ManifestLoadError,
|
|
3927
4582
|
ManifestLoader,
|
|
3928
4583
|
MissingHandlerError,
|
|
@@ -3930,32 +4585,63 @@ function identityChipStyle(accent) {
|
|
|
3930
4585
|
SA_LOCALES,
|
|
3931
4586
|
SA_LOCALE_LABELS,
|
|
3932
4587
|
SA_MESSAGES,
|
|
4588
|
+
adminErrorMessage,
|
|
4589
|
+
auditResource,
|
|
4590
|
+
bindResource,
|
|
3933
4591
|
buildRoutes,
|
|
3934
4592
|
buildSidebar,
|
|
4593
|
+
clampListPage,
|
|
4594
|
+
clampListPageSize,
|
|
3935
4595
|
countPlans,
|
|
3936
4596
|
createAdminResourceClient,
|
|
4597
|
+
createAxiosHttpClient,
|
|
4598
|
+
createFetchHttpClient,
|
|
3937
4599
|
createSaCatalog,
|
|
3938
4600
|
defaultHttpClient,
|
|
3939
4601
|
defaultKvStore,
|
|
3940
4602
|
defaultSectionOrder,
|
|
4603
|
+
defineListOp,
|
|
3941
4604
|
defineMessages,
|
|
4605
|
+
defineResource,
|
|
4606
|
+
filterQueryString,
|
|
3942
4607
|
formatCurrency,
|
|
3943
4608
|
formatMessage,
|
|
3944
4609
|
getJson,
|
|
3945
4610
|
identityAccentAt,
|
|
3946
4611
|
identityAccentFor,
|
|
3947
4612
|
identityChipStyle,
|
|
4613
|
+
isAdminError,
|
|
4614
|
+
isAxiosNoResponseError,
|
|
3948
4615
|
isCurrentlyValid,
|
|
4616
|
+
isEmptyResponse,
|
|
3949
4617
|
isExpired,
|
|
3950
4618
|
isFutureScheduled,
|
|
4619
|
+
isPlatformError,
|
|
3951
4620
|
isProductionBoot,
|
|
3952
4621
|
isSaBuiltinLocale,
|
|
4622
|
+
isSentInQuery,
|
|
4623
|
+
isTransportFailure,
|
|
4624
|
+
listUrl,
|
|
4625
|
+
markEmptyResponse,
|
|
4626
|
+
markPlatformError,
|
|
4627
|
+
markTransportFailure,
|
|
3953
4628
|
mergeMessages,
|
|
4629
|
+
planVersionsResource,
|
|
4630
|
+
plansResource,
|
|
4631
|
+
platformResources,
|
|
3954
4632
|
postJson,
|
|
4633
|
+
readErrorCode,
|
|
4634
|
+
readErrorDetail,
|
|
4635
|
+
readListPage,
|
|
4636
|
+
requestJson,
|
|
4637
|
+
requestJsonBody,
|
|
4638
|
+
requireServerAnswer,
|
|
3955
4639
|
resolveExtension,
|
|
3956
4640
|
resolveLoginBranding,
|
|
3957
4641
|
resolveMessages,
|
|
3958
4642
|
resolvePlans,
|
|
4643
|
+
tenantsResource,
|
|
4644
|
+
toAdminError,
|
|
3959
4645
|
todayIsoDate,
|
|
3960
4646
|
trimTrailingSlashes
|
|
3961
4647
|
});
|