tempest-express-sdk 0.25.0 → 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 +1 -1
- package/dist/{chunk-TIW2KPT2.js → chunk-GLZYNX63.js} +3 -3
- package/dist/{chunk-TIW2KPT2.js.map → chunk-GLZYNX63.js.map} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +394 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +380 -95
- package/dist/index.d.ts +380 -95
- package/dist/index.js +390 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9444,12 +9444,51 @@ var MessagingHub = class {
|
|
|
9444
9444
|
}
|
|
9445
9445
|
};
|
|
9446
9446
|
|
|
9447
|
+
// src/admin/dashboard.ts
|
|
9448
|
+
function metricCard(label, compute, helpText) {
|
|
9449
|
+
return helpText === void 0 ? { label, compute } : { label, compute, helpText };
|
|
9450
|
+
}
|
|
9451
|
+
function trendPercent(trend) {
|
|
9452
|
+
if (trend.previous === 0) return null;
|
|
9453
|
+
return (trend.value - trend.previous) / trend.previous * 100;
|
|
9454
|
+
}
|
|
9455
|
+
function trendDirection(trend) {
|
|
9456
|
+
if (trend.value > trend.previous) return "up";
|
|
9457
|
+
if (trend.value < trend.previous) return "down";
|
|
9458
|
+
return "flat";
|
|
9459
|
+
}
|
|
9460
|
+
function partitionTotal(partition) {
|
|
9461
|
+
return partition.segments.reduce((total, segment) => total + segment.value, 0);
|
|
9462
|
+
}
|
|
9463
|
+
|
|
9464
|
+
// src/admin/lenses.ts
|
|
9465
|
+
function slugify(name) {
|
|
9466
|
+
return name.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "lens";
|
|
9467
|
+
}
|
|
9468
|
+
function adminLens(options) {
|
|
9469
|
+
return {
|
|
9470
|
+
name: options.name,
|
|
9471
|
+
slug: slugify(options.name),
|
|
9472
|
+
label: options.label ?? options.name,
|
|
9473
|
+
filters: options.filters ?? {},
|
|
9474
|
+
orderBy: options.orderBy ?? null
|
|
9475
|
+
};
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
// src/admin/permissions.ts
|
|
9479
|
+
var AdminPermission = {
|
|
9480
|
+
VIEW: "view",
|
|
9481
|
+
CREATE: "create",
|
|
9482
|
+
EDIT: "edit",
|
|
9483
|
+
DELETE: "delete"
|
|
9484
|
+
};
|
|
9485
|
+
|
|
9447
9486
|
// src/admin/actions.ts
|
|
9448
|
-
function
|
|
9487
|
+
function slugify2(label) {
|
|
9449
9488
|
return label.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
|
|
9450
9489
|
}
|
|
9451
9490
|
function adminAction(options, handler) {
|
|
9452
|
-
const name = options.name ??
|
|
9491
|
+
const name = options.name ?? slugify2(options.label);
|
|
9453
9492
|
if (name === "") throw new Error("adminAction requires a non-empty name or label");
|
|
9454
9493
|
return {
|
|
9455
9494
|
name,
|
|
@@ -9555,6 +9594,12 @@ var NEVER_EDITABLE = [
|
|
|
9555
9594
|
"hashedPassword"
|
|
9556
9595
|
];
|
|
9557
9596
|
var NEVER_LISTED = ["hashedPassword"];
|
|
9597
|
+
var AUDIT_FIELDS = [
|
|
9598
|
+
"createdAt",
|
|
9599
|
+
"updatedAt",
|
|
9600
|
+
"createdBy",
|
|
9601
|
+
"updatedBy"
|
|
9602
|
+
];
|
|
9558
9603
|
var AdminModel = class {
|
|
9559
9604
|
/** The managed model class. */
|
|
9560
9605
|
model;
|
|
@@ -9578,6 +9623,10 @@ var AdminModel = class {
|
|
|
9578
9623
|
canEdit;
|
|
9579
9624
|
/** Whether the delete action is exposed. */
|
|
9580
9625
|
canDelete;
|
|
9626
|
+
/** Audit-log model backing the detail timeline, or `null`. */
|
|
9627
|
+
auditModel;
|
|
9628
|
+
/** Saved list-view presets, in declaration order. */
|
|
9629
|
+
lenses;
|
|
9581
9630
|
actions = /* @__PURE__ */ new Map();
|
|
9582
9631
|
slugOverride;
|
|
9583
9632
|
listDisplayOverride;
|
|
@@ -9603,6 +9652,8 @@ var AdminModel = class {
|
|
|
9603
9652
|
this.canCreate = options.canCreate ?? true;
|
|
9604
9653
|
this.canEdit = options.canEdit ?? true;
|
|
9605
9654
|
this.canDelete = options.canDelete ?? true;
|
|
9655
|
+
this.auditModel = options.auditModel ?? null;
|
|
9656
|
+
this.lenses = [...options.lenses ?? []];
|
|
9606
9657
|
for (const action of options.actions ?? []) {
|
|
9607
9658
|
if (this.actions.has(action.name)) {
|
|
9608
9659
|
throw new Error(
|
|
@@ -9687,6 +9738,25 @@ var AdminModel = class {
|
|
|
9687
9738
|
if (this.listDisplayOverride !== null) return [...this.listDisplayOverride];
|
|
9688
9739
|
return this.columnNames().filter((name) => !NEVER_LISTED.includes(name));
|
|
9689
9740
|
}
|
|
9741
|
+
/**
|
|
9742
|
+
* Look a lens up by its slug.
|
|
9743
|
+
*
|
|
9744
|
+
* @param slug - The `?lens=` value.
|
|
9745
|
+
* @returns The lens, or `null` when nothing matches.
|
|
9746
|
+
*/
|
|
9747
|
+
getLens(slug) {
|
|
9748
|
+
return this.lenses.find((lens) => lens.slug === slug) ?? null;
|
|
9749
|
+
}
|
|
9750
|
+
/**
|
|
9751
|
+
* Return the audit/timestamp columns the model actually declares.
|
|
9752
|
+
*
|
|
9753
|
+
* @returns The subset of `createdAt` / `updatedAt` / `createdBy` /
|
|
9754
|
+
* `updatedBy` present on the model, in that order.
|
|
9755
|
+
*/
|
|
9756
|
+
auditFieldNames() {
|
|
9757
|
+
const known = new Set(this.columnNames());
|
|
9758
|
+
return AUDIT_FIELDS.filter((name) => known.has(name));
|
|
9759
|
+
}
|
|
9690
9760
|
/**
|
|
9691
9761
|
* Return the columns the detail view renders.
|
|
9692
9762
|
*
|
|
@@ -9695,10 +9765,16 @@ var AdminModel = class {
|
|
|
9695
9765
|
* where an operator goes to see the whole record, so trimming it there would
|
|
9696
9766
|
* hide data with nowhere else to read it.
|
|
9697
9767
|
*
|
|
9698
|
-
*
|
|
9768
|
+
* The audit/timestamp columns are held back too — they render in the detail
|
|
9769
|
+
* view's own audit panel, next to the change history, rather than scattered
|
|
9770
|
+
* among the domain fields.
|
|
9771
|
+
*
|
|
9772
|
+
* @returns Every domain column, in declaration order.
|
|
9699
9773
|
*/
|
|
9700
9774
|
detailFieldNames() {
|
|
9701
|
-
return this.columnNames().filter(
|
|
9775
|
+
return this.columnNames().filter(
|
|
9776
|
+
(name) => !NEVER_LISTED.includes(name) && !AUDIT_FIELDS.includes(name)
|
|
9777
|
+
);
|
|
9702
9778
|
}
|
|
9703
9779
|
/**
|
|
9704
9780
|
* Return the columns a create/edit form exposes.
|
|
@@ -10155,6 +10231,8 @@ var AdminSite = class {
|
|
|
10155
10231
|
siteUrl;
|
|
10156
10232
|
/** Typed appearance overrides. */
|
|
10157
10233
|
theme;
|
|
10234
|
+
/** Business-metric cards rendered at the top of the dashboard. */
|
|
10235
|
+
dashboardCards;
|
|
10158
10236
|
registry = /* @__PURE__ */ new Map();
|
|
10159
10237
|
/**
|
|
10160
10238
|
* Initialize the site.
|
|
@@ -10167,6 +10245,7 @@ var AdminSite = class {
|
|
|
10167
10245
|
this.indexSubtitle = options.indexSubtitle ?? "Site administration";
|
|
10168
10246
|
this.siteUrl = options.siteUrl ?? null;
|
|
10169
10247
|
this.theme = options.theme ?? {};
|
|
10248
|
+
this.dashboardCards = [...options.dashboardCards ?? []];
|
|
10170
10249
|
}
|
|
10171
10250
|
/**
|
|
10172
10251
|
* Return the centered header brand text.
|
|
@@ -11986,7 +12065,7 @@ function renderMfaPage(context, error) {
|
|
|
11986
12065
|
</section>`;
|
|
11987
12066
|
return renderLayout(context, `Two-factor \xB7 ${context.site.title}`, body);
|
|
11988
12067
|
}
|
|
11989
|
-
function renderDashboardPage(context, cards, metrics) {
|
|
12068
|
+
function renderDashboardPage(context, cards, metrics, businessCards = []) {
|
|
11990
12069
|
const metricsPanel = metrics === null ? "" : `<div class="tempest-admin-stats" aria-label="System metrics">
|
|
11991
12070
|
<div class="tempest-admin-stat">
|
|
11992
12071
|
<span class="tempest-admin-stat__label">CPU</span>
|
|
@@ -11998,6 +12077,7 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
11998
12077
|
<span class="tempest-admin-stat__sub">${escapeHtml(metrics.memoryUsedGb)} / ${escapeHtml(metrics.memoryTotalGb)} GB</span>
|
|
11999
12078
|
</div>
|
|
12000
12079
|
</div>`;
|
|
12080
|
+
const business = businessCards.length > 0 ? `<div class="tempest-admin-cards" aria-label="Business metrics">${businessCards.map(renderBusinessCard).join("")}</div>` : "";
|
|
12001
12081
|
const models = cards.length > 0 ? `<div class="tempest-admin-models">${cards.map(
|
|
12002
12082
|
(card) => `<article class="tempest-admin-model-card">
|
|
12003
12083
|
<header class="tempest-admin-model-card__head">
|
|
@@ -12014,10 +12094,53 @@ function renderDashboardPage(context, cards, metrics) {
|
|
|
12014
12094
|
<h1>${escapeHtml(context.site.title)}</h1>
|
|
12015
12095
|
<p>${escapeHtml(context.site.indexSubtitle)}</p>
|
|
12016
12096
|
${metricsPanel}
|
|
12097
|
+
${business}
|
|
12017
12098
|
${models}
|
|
12018
12099
|
</section>`;
|
|
12019
12100
|
return renderLayout(context, context.site.title, body);
|
|
12020
12101
|
}
|
|
12102
|
+
function renderBusinessCard(card) {
|
|
12103
|
+
const help = card.helpText !== null ? `<span class="tempest-admin-card__help">${escapeHtml(card.helpText)}</span>` : "";
|
|
12104
|
+
if (card.error !== null) {
|
|
12105
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
12106
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12107
|
+
<span class="tempest-admin-card__value">\u2014</span>
|
|
12108
|
+
<span class="tempest-admin-card__help">${escapeHtml(card.error)}</span>
|
|
12109
|
+
</article>`;
|
|
12110
|
+
}
|
|
12111
|
+
const unit = card.unit !== null ? ` <small>${escapeHtml(card.unit)}</small>` : "";
|
|
12112
|
+
if (card.kind === "partition") {
|
|
12113
|
+
const parts = card.segments.map(
|
|
12114
|
+
(segment) => `<li>
|
|
12115
|
+
<span class="tempest-admin-card__part-label">${escapeHtml(segment.label)}</span>
|
|
12116
|
+
<span class="tempest-admin-card__part-bar"><span style="width: ${Math.round(segment.percent)}%"></span></span>
|
|
12117
|
+
<span class="tempest-admin-card__part-value">${escapeHtml(segment.value)}</span>
|
|
12118
|
+
</li>`
|
|
12119
|
+
).join("");
|
|
12120
|
+
return `<article class="tempest-admin-card tempest-admin-card--partition">
|
|
12121
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12122
|
+
<ul class="tempest-admin-card__parts">${parts}</ul>
|
|
12123
|
+
${help}
|
|
12124
|
+
</article>`;
|
|
12125
|
+
}
|
|
12126
|
+
if (card.kind === "trend") {
|
|
12127
|
+
const arrow = card.direction === "up" ? "\u25B2" : card.direction === "down" ? "\u25BC" : "\u25AC";
|
|
12128
|
+
return `<article class="tempest-admin-card tempest-admin-card--trend">
|
|
12129
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12130
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12131
|
+
<span class="tempest-admin-card__trend tempest-admin-card__trend--${escapeHtml(card.direction)}">
|
|
12132
|
+
${arrow} ${card.percent === null ? "\u2014" : escapeHtml(card.percent)}
|
|
12133
|
+
<small>vs prev ${escapeHtml(card.previous)}</small>
|
|
12134
|
+
</span>
|
|
12135
|
+
${help}
|
|
12136
|
+
</article>`;
|
|
12137
|
+
}
|
|
12138
|
+
return `<article class="tempest-admin-card tempest-admin-card--value">
|
|
12139
|
+
<span class="tempest-admin-card__label">${escapeHtml(card.label)}</span>
|
|
12140
|
+
<span class="tempest-admin-card__value">${escapeHtml(card.value)}${unit}</span>
|
|
12141
|
+
${help}
|
|
12142
|
+
</article>`;
|
|
12143
|
+
}
|
|
12021
12144
|
function renderFilter(filter) {
|
|
12022
12145
|
const name = `filter_${filter.field}`;
|
|
12023
12146
|
if (filter.kind === "select") {
|
|
@@ -12087,6 +12210,9 @@ function renderListPage(context, view) {
|
|
|
12087
12210
|
<a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
|
|
12088
12211
|
</div>
|
|
12089
12212
|
</div>
|
|
12213
|
+
${view.lenses.length > 0 ? `<nav class="tempest-admin-lenses" aria-label="Lenses">${view.lenses.map(
|
|
12214
|
+
(lens) => `<a class="tempest-admin-lens${lens.active ? " tempest-admin-lens--active" : ""}" href="${escapeHtml(lens.url)}">${escapeHtml(lens.label)}</a>`
|
|
12215
|
+
).join("")}</nav>` : ""}
|
|
12090
12216
|
${bulkBar}
|
|
12091
12217
|
<div class="tempest-admin-table-wrap">
|
|
12092
12218
|
<table class="tempest-admin-list__table">
|
|
@@ -12110,6 +12236,7 @@ function renderDetailPage(context, view) {
|
|
|
12110
12236
|
const fields = view.fields.map(
|
|
12111
12237
|
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
12112
12238
|
).join("");
|
|
12239
|
+
const auditPanel = view.audit === null ? "" : renderAuditPanel(view.audit);
|
|
12113
12240
|
const body = `<section class="tempest-admin-detail">
|
|
12114
12241
|
<header class="tempest-admin-detail__header">
|
|
12115
12242
|
<h1>${escapeHtml(view.title)} \xB7 ${escapeHtml(view.identity)}</h1>
|
|
@@ -12123,9 +12250,37 @@ function renderDetailPage(context, view) {
|
|
|
12123
12250
|
</div>
|
|
12124
12251
|
</header>
|
|
12125
12252
|
<dl class="tempest-admin-detail__fields">${fields}</dl>
|
|
12253
|
+
${auditPanel}
|
|
12126
12254
|
</section>`;
|
|
12127
12255
|
return renderLayout(context, `${view.title} \xB7 ${view.identity}`, body);
|
|
12128
12256
|
}
|
|
12257
|
+
function renderAuditPanel(audit) {
|
|
12258
|
+
const rows = audit.fields.map(
|
|
12259
|
+
(field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
|
|
12260
|
+
).join("");
|
|
12261
|
+
const history = audit.history.length > 0 ? `<ol class="tempest-admin-history">${audit.history.map((entry) => {
|
|
12262
|
+
const changes = entry.changes.length > 0 ? `<table class="tempest-admin-history__changes"><thead><tr><th>Field</th><th>Before</th><th>After</th></tr></thead><tbody>${entry.changes.map(
|
|
12263
|
+
(change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
|
|
12264
|
+
).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
|
|
12265
|
+
const context = entry.context !== null ? `<pre class="tempest-admin-detail__json">${escapeHtml(entry.context)}</pre>` : "";
|
|
12266
|
+
return `<li class="tempest-admin-history__item">
|
|
12267
|
+
<details>
|
|
12268
|
+
<summary>
|
|
12269
|
+
<span class="tempest-admin-history__action">${escapeHtml(entry.action)}</span>
|
|
12270
|
+
<span class="tempest-admin-history__actor">${escapeHtml(entry.actor)}</span>
|
|
12271
|
+
<span class="tempest-admin-history__at">${escapeHtml(entry.at)}</span>
|
|
12272
|
+
</summary>
|
|
12273
|
+
${changes}
|
|
12274
|
+
${context}
|
|
12275
|
+
</details>
|
|
12276
|
+
</li>`;
|
|
12277
|
+
}).join("")}</ol>` : "";
|
|
12278
|
+
return `<section class="tempest-admin-audit">
|
|
12279
|
+
<h2>Audit</h2>
|
|
12280
|
+
<dl class="tempest-admin-detail__fields">${rows}</dl>
|
|
12281
|
+
${history}
|
|
12282
|
+
</section>`;
|
|
12283
|
+
}
|
|
12129
12284
|
function renderFormField(field) {
|
|
12130
12285
|
const required = field.required ? " required" : "";
|
|
12131
12286
|
const name = escapeHtml(field.name);
|
|
@@ -12189,6 +12344,7 @@ function renderFormPage(context, view) {
|
|
|
12189
12344
|
return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
|
|
12190
12345
|
}
|
|
12191
12346
|
var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
|
|
12347
|
+
var AUDIT_HISTORY_LIMIT = 50;
|
|
12192
12348
|
var FK_OPTION_CAP = 1e3;
|
|
12193
12349
|
var FLASH_MAX_LENGTH = 300;
|
|
12194
12350
|
var FLASH_MESSAGES = {
|
|
@@ -12227,18 +12383,23 @@ function makeAdminRouter(site, options) {
|
|
|
12227
12383
|
const backend = options.authBackend;
|
|
12228
12384
|
const router = express3__default.default.Router();
|
|
12229
12385
|
router.use(prefix, express3__default.default.urlencoded({ extended: false }));
|
|
12230
|
-
const context = (req, session) => ({
|
|
12386
|
+
const context = (req, session, models = site.list()) => ({
|
|
12231
12387
|
site,
|
|
12232
12388
|
theme,
|
|
12233
12389
|
prefix,
|
|
12234
12390
|
session,
|
|
12235
12391
|
currentPath: req.originalUrl.split("?")[0] ?? req.path,
|
|
12236
|
-
navModels:
|
|
12392
|
+
navModels: models.map((admin) => ({
|
|
12237
12393
|
label: admin.verboseNamePlural(),
|
|
12238
12394
|
url: `${prefix}/m/${admin.slug()}`
|
|
12239
12395
|
})),
|
|
12240
12396
|
messages: flashFor(req)
|
|
12241
12397
|
});
|
|
12398
|
+
const allows = async (principal, admin, action) => {
|
|
12399
|
+
if (!flagAllows(admin, action)) return false;
|
|
12400
|
+
if (options.accessPolicy === void 0) return true;
|
|
12401
|
+
return Boolean(await options.accessPolicy(principal, admin, action));
|
|
12402
|
+
};
|
|
12242
12403
|
const flashFor = (req) => {
|
|
12243
12404
|
const fixed = FLASH_MESSAGES[queryString(req.query.ok)];
|
|
12244
12405
|
if (fixed !== void 0) return [fixed];
|
|
@@ -12286,14 +12447,28 @@ function makeAdminRouter(site, options) {
|
|
|
12286
12447
|
res.redirect(`${prefix}/mfa`);
|
|
12287
12448
|
return null;
|
|
12288
12449
|
}
|
|
12289
|
-
|
|
12450
|
+
const visible = [];
|
|
12451
|
+
for (const admin of site.list()) {
|
|
12452
|
+
if (await allows(principal, admin, AdminPermission.VIEW)) visible.push(admin);
|
|
12453
|
+
}
|
|
12454
|
+
return { session, dbSession, principal, visible };
|
|
12290
12455
|
};
|
|
12291
|
-
const resolveAdmin = (req, res, state) => {
|
|
12456
|
+
const resolveAdmin = async (req, res, state, action = AdminPermission.VIEW) => {
|
|
12292
12457
|
const admin = site.get(String(req.params.slug));
|
|
12293
|
-
if (admin === null) {
|
|
12294
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12458
|
+
if (admin === null || !await allows(state.principal, admin, AdminPermission.VIEW)) {
|
|
12459
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12295
12460
|
return null;
|
|
12296
12461
|
}
|
|
12462
|
+
if (action !== AdminPermission.VIEW) {
|
|
12463
|
+
if (!flagAllows(admin, action)) {
|
|
12464
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12465
|
+
return null;
|
|
12466
|
+
}
|
|
12467
|
+
if (!await allows(state.principal, admin, action)) {
|
|
12468
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12469
|
+
return null;
|
|
12470
|
+
}
|
|
12471
|
+
}
|
|
12297
12472
|
return admin;
|
|
12298
12473
|
};
|
|
12299
12474
|
const renderNotFound = (ctx) => renderDashboardPage(
|
|
@@ -12304,7 +12479,7 @@ function makeAdminRouter(site, options) {
|
|
|
12304
12479
|
const checkCsrf = (req, res, state) => {
|
|
12305
12480
|
const body = req.body;
|
|
12306
12481
|
if (csrfTokenMatches(state.session, body?.csrf_token)) return true;
|
|
12307
|
-
html(res, renderNotFound(context(req, state.session)), 403);
|
|
12482
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12308
12483
|
return false;
|
|
12309
12484
|
};
|
|
12310
12485
|
router.get(`${prefix}/static/admin.css`, (_req, res) => {
|
|
@@ -12386,7 +12561,7 @@ function makeAdminRouter(site, options) {
|
|
|
12386
12561
|
const state = await authenticate(req, res);
|
|
12387
12562
|
if (state === null) return;
|
|
12388
12563
|
const cards = [];
|
|
12389
|
-
for (const admin of
|
|
12564
|
+
for (const admin of state.visible) {
|
|
12390
12565
|
let count = null;
|
|
12391
12566
|
try {
|
|
12392
12567
|
count = await admin.repository(state.dbSession).count();
|
|
@@ -12400,9 +12575,13 @@ function makeAdminRouter(site, options) {
|
|
|
12400
12575
|
label: admin.verboseNamePlural(),
|
|
12401
12576
|
count,
|
|
12402
12577
|
url: `${prefix}/m/${admin.slug()}`,
|
|
12403
|
-
newUrl: admin.
|
|
12578
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null
|
|
12404
12579
|
});
|
|
12405
12580
|
}
|
|
12581
|
+
const businessCards = [];
|
|
12582
|
+
for (const card of site.dashboardCards) {
|
|
12583
|
+
businessCards.push(await computeBusinessCard(card, state.dbSession));
|
|
12584
|
+
}
|
|
12406
12585
|
let metrics = null;
|
|
12407
12586
|
if (showMetrics) {
|
|
12408
12587
|
const snapshot2 = MetricsUtils.system();
|
|
@@ -12413,7 +12592,15 @@ function makeAdminRouter(site, options) {
|
|
|
12413
12592
|
memoryTotalGb: (snapshot2.memory.total / 1024 ** 3).toFixed(1)
|
|
12414
12593
|
};
|
|
12415
12594
|
}
|
|
12416
|
-
html(
|
|
12595
|
+
html(
|
|
12596
|
+
res,
|
|
12597
|
+
renderDashboardPage(
|
|
12598
|
+
context(req, state.session, state.visible),
|
|
12599
|
+
cards,
|
|
12600
|
+
metrics,
|
|
12601
|
+
businessCards
|
|
12602
|
+
)
|
|
12603
|
+
);
|
|
12417
12604
|
})
|
|
12418
12605
|
);
|
|
12419
12606
|
router.get(
|
|
@@ -12421,7 +12608,7 @@ function makeAdminRouter(site, options) {
|
|
|
12421
12608
|
guarded(async (req, res) => {
|
|
12422
12609
|
const state = await authenticate(req, res);
|
|
12423
12610
|
if (state === null) return;
|
|
12424
|
-
const admin = resolveAdmin(req, res, state);
|
|
12611
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12425
12612
|
if (admin === null) return;
|
|
12426
12613
|
html(res, await renderList(req, admin, state));
|
|
12427
12614
|
})
|
|
@@ -12431,15 +12618,15 @@ function makeAdminRouter(site, options) {
|
|
|
12431
12618
|
guarded(async (req, res) => {
|
|
12432
12619
|
const state = await authenticate(req, res);
|
|
12433
12620
|
if (state === null) return;
|
|
12434
|
-
const admin = resolveAdmin(req, res, state);
|
|
12621
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12435
12622
|
if (admin === null) return;
|
|
12436
12623
|
if (!admin.canCreate) {
|
|
12437
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12624
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12438
12625
|
return;
|
|
12439
12626
|
}
|
|
12440
12627
|
html(
|
|
12441
12628
|
res,
|
|
12442
|
-
renderFormPage(context(req, state.session), {
|
|
12629
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12443
12630
|
mode: "create",
|
|
12444
12631
|
title: admin.verboseName(),
|
|
12445
12632
|
fields: buildFormFields(admin, {
|
|
@@ -12457,10 +12644,10 @@ function makeAdminRouter(site, options) {
|
|
|
12457
12644
|
guarded(async (req, res) => {
|
|
12458
12645
|
const state = await authenticate(req, res);
|
|
12459
12646
|
if (state === null) return;
|
|
12460
|
-
const admin = resolveAdmin(req, res, state);
|
|
12647
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.CREATE);
|
|
12461
12648
|
if (admin === null) return;
|
|
12462
12649
|
if (!admin.canCreate) {
|
|
12463
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12650
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12464
12651
|
return;
|
|
12465
12652
|
}
|
|
12466
12653
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12470,7 +12657,7 @@ function makeAdminRouter(site, options) {
|
|
|
12470
12657
|
const rerender = (error, status) => {
|
|
12471
12658
|
html(
|
|
12472
12659
|
res,
|
|
12473
|
-
renderFormPage(context(req, state.session), {
|
|
12660
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12474
12661
|
mode: "create",
|
|
12475
12662
|
title: admin.verboseName(),
|
|
12476
12663
|
fields: buildFormFields(admin, {
|
|
@@ -12489,6 +12676,7 @@ function makeAdminRouter(site, options) {
|
|
|
12489
12676
|
rerender("Please fix the highlighted fields.", 400);
|
|
12490
12677
|
return;
|
|
12491
12678
|
}
|
|
12679
|
+
stampActor(admin, parsed.data, backend.principalId(state.principal), true);
|
|
12492
12680
|
try {
|
|
12493
12681
|
await admin.repository(state.dbSession).create(parsed.data);
|
|
12494
12682
|
} catch (error) {
|
|
@@ -12503,11 +12691,11 @@ function makeAdminRouter(site, options) {
|
|
|
12503
12691
|
guarded(async (req, res) => {
|
|
12504
12692
|
const state = await authenticate(req, res);
|
|
12505
12693
|
if (state === null) return;
|
|
12506
|
-
const admin = resolveAdmin(req, res, state);
|
|
12694
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12507
12695
|
if (admin === null) return;
|
|
12508
12696
|
const format = String(req.params.format);
|
|
12509
12697
|
if (format !== "csv" && format !== "json") {
|
|
12510
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12698
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12511
12699
|
return;
|
|
12512
12700
|
}
|
|
12513
12701
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12529,7 +12717,7 @@ function makeAdminRouter(site, options) {
|
|
|
12529
12717
|
guarded(async (req, res) => {
|
|
12530
12718
|
const state = await authenticate(req, res);
|
|
12531
12719
|
if (state === null) return;
|
|
12532
|
-
const admin = resolveAdmin(req, res, state);
|
|
12720
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12533
12721
|
if (admin === null) return;
|
|
12534
12722
|
if (!checkCsrf(req, res, state)) return;
|
|
12535
12723
|
const body = req.body;
|
|
@@ -12545,7 +12733,12 @@ function makeAdminRouter(site, options) {
|
|
|
12545
12733
|
return;
|
|
12546
12734
|
}
|
|
12547
12735
|
if (!bulkActionsFor(admin).some((option) => option.value === action)) {
|
|
12548
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12736
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12737
|
+
return;
|
|
12738
|
+
}
|
|
12739
|
+
const needed = action === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
12740
|
+
if (!await allows(state.principal, admin, needed)) {
|
|
12741
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 403);
|
|
12549
12742
|
return;
|
|
12550
12743
|
}
|
|
12551
12744
|
const repository = admin.repository(state.dbSession);
|
|
@@ -12553,7 +12746,7 @@ function makeAdminRouter(site, options) {
|
|
|
12553
12746
|
if (action.startsWith("custom:")) {
|
|
12554
12747
|
const custom = admin.getAction(action.slice("custom:".length));
|
|
12555
12748
|
if (custom === null) {
|
|
12556
|
-
html(res, renderNotFound(context(req, state.session)), 400);
|
|
12749
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 400);
|
|
12557
12750
|
return;
|
|
12558
12751
|
}
|
|
12559
12752
|
let result;
|
|
@@ -12603,23 +12796,24 @@ function makeAdminRouter(site, options) {
|
|
|
12603
12796
|
guarded(async (req, res) => {
|
|
12604
12797
|
const state = await authenticate(req, res);
|
|
12605
12798
|
if (state === null) return;
|
|
12606
|
-
const admin = resolveAdmin(req, res, state);
|
|
12799
|
+
const admin = await resolveAdmin(req, res, state);
|
|
12607
12800
|
if (admin === null) return;
|
|
12608
12801
|
const row = await findRow(admin, state.dbSession, String(req.params.identity));
|
|
12609
12802
|
if (row === null) {
|
|
12610
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12803
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12611
12804
|
return;
|
|
12612
12805
|
}
|
|
12613
12806
|
const identity = String(row[admin.identityField]);
|
|
12614
12807
|
html(
|
|
12615
12808
|
res,
|
|
12616
|
-
renderDetailPage(context(req, state.session), {
|
|
12809
|
+
renderDetailPage(context(req, state.session, state.visible), {
|
|
12617
12810
|
title: admin.verboseName(),
|
|
12618
12811
|
identity,
|
|
12812
|
+
audit: await buildAuditView(admin, row, state.dbSession),
|
|
12619
12813
|
fields: admin.detailFieldNames().map((name) => ({ label: name, value: formatCellValue(row[name]) })),
|
|
12620
12814
|
backUrl: `${prefix}/m/${admin.slug()}`,
|
|
12621
|
-
editUrl: admin.
|
|
12622
|
-
deleteUrl: admin.
|
|
12815
|
+
editUrl: await allows(state.principal, admin, AdminPermission.EDIT) ? `${prefix}/m/${admin.slug()}/${identity}/edit` : null,
|
|
12816
|
+
deleteUrl: await allows(state.principal, admin, AdminPermission.DELETE) ? `${prefix}/m/${admin.slug()}/${identity}/delete` : null
|
|
12623
12817
|
})
|
|
12624
12818
|
);
|
|
12625
12819
|
})
|
|
@@ -12629,17 +12823,17 @@ function makeAdminRouter(site, options) {
|
|
|
12629
12823
|
guarded(async (req, res) => {
|
|
12630
12824
|
const state = await authenticate(req, res);
|
|
12631
12825
|
if (state === null) return;
|
|
12632
|
-
const admin = resolveAdmin(req, res, state);
|
|
12826
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12633
12827
|
if (admin === null) return;
|
|
12634
12828
|
const identity = String(req.params.identity);
|
|
12635
12829
|
const row = admin.canEdit ? await findRow(admin, state.dbSession, identity) : null;
|
|
12636
12830
|
if (row === null) {
|
|
12637
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12831
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12638
12832
|
return;
|
|
12639
12833
|
}
|
|
12640
12834
|
html(
|
|
12641
12835
|
res,
|
|
12642
|
-
renderFormPage(context(req, state.session), {
|
|
12836
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12643
12837
|
mode: "edit",
|
|
12644
12838
|
title: admin.verboseName(),
|
|
12645
12839
|
fields: buildFormFields(admin, {
|
|
@@ -12658,11 +12852,11 @@ function makeAdminRouter(site, options) {
|
|
|
12658
12852
|
guarded(async (req, res) => {
|
|
12659
12853
|
const state = await authenticate(req, res);
|
|
12660
12854
|
if (state === null) return;
|
|
12661
|
-
const admin = resolveAdmin(req, res, state);
|
|
12855
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.EDIT);
|
|
12662
12856
|
if (admin === null) return;
|
|
12663
12857
|
const identity = String(req.params.identity);
|
|
12664
12858
|
if (!admin.canEdit) {
|
|
12665
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12859
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12666
12860
|
return;
|
|
12667
12861
|
}
|
|
12668
12862
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12672,7 +12866,7 @@ function makeAdminRouter(site, options) {
|
|
|
12672
12866
|
const rerender = (error, status) => {
|
|
12673
12867
|
html(
|
|
12674
12868
|
res,
|
|
12675
|
-
renderFormPage(context(req, state.session), {
|
|
12869
|
+
renderFormPage(context(req, state.session, state.visible), {
|
|
12676
12870
|
mode: "edit",
|
|
12677
12871
|
title: admin.verboseName(),
|
|
12678
12872
|
fields: buildFormFields(admin, {
|
|
@@ -12691,10 +12885,16 @@ function makeAdminRouter(site, options) {
|
|
|
12691
12885
|
rerender("Please fix the highlighted fields.", 400);
|
|
12692
12886
|
return;
|
|
12693
12887
|
}
|
|
12888
|
+
stampActor(
|
|
12889
|
+
admin,
|
|
12890
|
+
parsed.data,
|
|
12891
|
+
backend.principalId(state.principal),
|
|
12892
|
+
false
|
|
12893
|
+
);
|
|
12694
12894
|
try {
|
|
12695
12895
|
const changed = await admin.repository(state.dbSession).update({ [admin.identityField]: identity }, parsed.data);
|
|
12696
12896
|
if (changed === 0) {
|
|
12697
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12897
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12698
12898
|
return;
|
|
12699
12899
|
}
|
|
12700
12900
|
} catch (error) {
|
|
@@ -12709,10 +12909,10 @@ function makeAdminRouter(site, options) {
|
|
|
12709
12909
|
guarded(async (req, res) => {
|
|
12710
12910
|
const state = await authenticate(req, res);
|
|
12711
12911
|
if (state === null) return;
|
|
12712
|
-
const admin = resolveAdmin(req, res, state);
|
|
12912
|
+
const admin = await resolveAdmin(req, res, state, AdminPermission.DELETE);
|
|
12713
12913
|
if (admin === null) return;
|
|
12714
12914
|
if (!admin.canDelete) {
|
|
12715
|
-
html(res, renderNotFound(context(req, state.session)), 404);
|
|
12915
|
+
html(res, renderNotFound(context(req, state.session, state.visible)), 404);
|
|
12716
12916
|
return;
|
|
12717
12917
|
}
|
|
12718
12918
|
if (!checkCsrf(req, res, state)) return;
|
|
@@ -12720,10 +12920,58 @@ function makeAdminRouter(site, options) {
|
|
|
12720
12920
|
res.redirect(`${prefix}/m/${admin.slug()}?ok=deleted`);
|
|
12721
12921
|
})
|
|
12722
12922
|
);
|
|
12923
|
+
async function actorLabel(actor, dbSession) {
|
|
12924
|
+
if (actor === null || actor === void 0 || actor === "") return "";
|
|
12925
|
+
const principal = await backend.loadPrincipal(dbSession, String(actor));
|
|
12926
|
+
return principal === null ? String(actor) : backend.displayName(principal);
|
|
12927
|
+
}
|
|
12928
|
+
async function buildAuditView(admin, row, dbSession) {
|
|
12929
|
+
const fields = [];
|
|
12930
|
+
for (const name of admin.auditFieldNames()) {
|
|
12931
|
+
const value = name === "createdBy" || name === "updatedBy" ? await actorLabel(row[name], dbSession) : formatCellValue(row[name]);
|
|
12932
|
+
fields.push({ label: humanizeField(name), value });
|
|
12933
|
+
}
|
|
12934
|
+
const history = [];
|
|
12935
|
+
if (admin.auditModel !== null) {
|
|
12936
|
+
const entity = admin.model.name;
|
|
12937
|
+
const entityId = String(row[admin.identityField]);
|
|
12938
|
+
const page2 = await new tempestDbJs.BaseRepository(admin.auditModel, dbSession).paginate({
|
|
12939
|
+
page: 1,
|
|
12940
|
+
pageSize: AUDIT_HISTORY_LIMIT,
|
|
12941
|
+
orderBy: "createdAt",
|
|
12942
|
+
ascending: false,
|
|
12943
|
+
filters: { entity, entityId }
|
|
12944
|
+
});
|
|
12945
|
+
for (const entry of page2.items) {
|
|
12946
|
+
const changes = entry.changes ?? {};
|
|
12947
|
+
history.push({
|
|
12948
|
+
action: String(entry.action ?? ""),
|
|
12949
|
+
at: formatCellValue(entry.createdAt),
|
|
12950
|
+
actor: await actorLabel(entry.actor, dbSession) || "\u2014",
|
|
12951
|
+
changes: Object.entries(changes).map(([field, change]) => ({
|
|
12952
|
+
field,
|
|
12953
|
+
before: formatCellValue(change?.before),
|
|
12954
|
+
after: formatCellValue(change?.after)
|
|
12955
|
+
})),
|
|
12956
|
+
context: entry.context === null || entry.context === void 0 ? null : JSON.stringify(entry.context, null, 2)
|
|
12957
|
+
});
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
if (fields.length === 0 && history.length === 0) return null;
|
|
12961
|
+
return { fields, history };
|
|
12962
|
+
}
|
|
12723
12963
|
async function findRow(admin, dbSession, identity) {
|
|
12724
12964
|
const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
|
|
12725
12965
|
return row ?? null;
|
|
12726
12966
|
}
|
|
12967
|
+
async function permittedBulkActions(admin, principal) {
|
|
12968
|
+
const permitted = [];
|
|
12969
|
+
for (const option of bulkActionsFor(admin)) {
|
|
12970
|
+
const needed = option.value === "delete" ? AdminPermission.DELETE : AdminPermission.EDIT;
|
|
12971
|
+
if (await allows(principal, admin, needed)) permitted.push(option);
|
|
12972
|
+
}
|
|
12973
|
+
return permitted;
|
|
12974
|
+
}
|
|
12727
12975
|
async function renderList(req, admin, state) {
|
|
12728
12976
|
const columns = adminColumns(admin.model);
|
|
12729
12977
|
const query = await resolveListQuery(req, admin, state.dbSession);
|
|
@@ -12778,21 +13026,36 @@ function makeAdminRouter(site, options) {
|
|
|
12778
13026
|
searchValue: query.search,
|
|
12779
13027
|
filters: query.filterViews,
|
|
12780
13028
|
sort,
|
|
12781
|
-
newUrl: admin.
|
|
12782
|
-
bulkActions:
|
|
13029
|
+
newUrl: await allows(state.principal, admin, AdminPermission.CREATE) ? `${prefix}/m/${admin.slug()}/new` : null,
|
|
13030
|
+
bulkActions: await permittedBulkActions(admin, state.principal),
|
|
12783
13031
|
bulkUrl: `${prefix}/m/${admin.slug()}/bulk`,
|
|
12784
13032
|
exportCsvUrl: exportUrl("csv"),
|
|
12785
|
-
exportJsonUrl: exportUrl("json")
|
|
13033
|
+
exportJsonUrl: exportUrl("json"),
|
|
13034
|
+
lenses: admin.lenses.length === 0 ? [] : [
|
|
13035
|
+
{
|
|
13036
|
+
label: "All",
|
|
13037
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: void 0 })}`,
|
|
13038
|
+
active: query.lens === ""
|
|
13039
|
+
},
|
|
13040
|
+
...admin.lenses.map((entry) => ({
|
|
13041
|
+
label: entry.label,
|
|
13042
|
+
url: `?${buildQuery({ ...query.baseQuery, lens: entry.slug })}`,
|
|
13043
|
+
active: query.lens === entry.slug
|
|
13044
|
+
}))
|
|
13045
|
+
]
|
|
12786
13046
|
};
|
|
12787
|
-
return renderListPage(context(req, state.session), view);
|
|
13047
|
+
return renderListPage(context(req, state.session, state.visible), view);
|
|
12788
13048
|
}
|
|
12789
13049
|
async function resolveListQuery(req, admin, dbSession) {
|
|
12790
13050
|
const columns = adminColumns(admin.model);
|
|
12791
13051
|
const search = queryString(req.query.q);
|
|
12792
13052
|
const sortField = queryString(req.query.sort);
|
|
12793
13053
|
const sortColumn = sortField in columns ? sortField : null;
|
|
12794
|
-
const
|
|
13054
|
+
const lens = admin.getLens(queryString(req.query.lens));
|
|
12795
13055
|
const conditions = [];
|
|
13056
|
+
if (lens !== null && Object.keys(lens.filters).length > 0) {
|
|
13057
|
+
conditions.push(lens.filters);
|
|
13058
|
+
}
|
|
12796
13059
|
const filterViews = [];
|
|
12797
13060
|
for (const field of admin.listFilter) {
|
|
12798
13061
|
const column6 = columns[field];
|
|
@@ -12856,15 +13119,21 @@ function makeAdminRouter(site, options) {
|
|
|
12856
13119
|
baseQuery[`filter_${view.field}`] = view.value;
|
|
12857
13120
|
}
|
|
12858
13121
|
}
|
|
13122
|
+
const lensOrder = lens?.orderBy ?? null;
|
|
13123
|
+
const lensDescending = lensOrder?.startsWith("-");
|
|
13124
|
+
const lensColumn = lensOrder === null ? null : lensOrder.replace(/^-/, "");
|
|
13125
|
+
const ascending = sortColumn !== null ? queryString(req.query.dir) !== "desc" : lensColumn !== null ? !lensDescending : admin.orderAscending;
|
|
13126
|
+
if (lens !== null) baseQuery.lens = lens.slug;
|
|
12859
13127
|
return {
|
|
12860
13128
|
search,
|
|
12861
13129
|
searchable,
|
|
12862
13130
|
where: conditions.length === 0 ? void 0 : tempestDbJs.and(...conditions),
|
|
12863
|
-
orderBy: sortColumn ?? admin.orderKey ?? void 0,
|
|
13131
|
+
orderBy: sortColumn ?? lensColumn ?? admin.orderKey ?? void 0,
|
|
12864
13132
|
ascending,
|
|
12865
13133
|
sortColumn,
|
|
12866
13134
|
filterViews,
|
|
12867
|
-
baseQuery
|
|
13135
|
+
baseQuery,
|
|
13136
|
+
lens: lens?.slug ?? ""
|
|
12868
13137
|
};
|
|
12869
13138
|
}
|
|
12870
13139
|
async function relatedOptions(column6, dbSession) {
|
|
@@ -12891,6 +13160,76 @@ function makeAdminRouter(site, options) {
|
|
|
12891
13160
|
}
|
|
12892
13161
|
return router;
|
|
12893
13162
|
}
|
|
13163
|
+
function flagAllows(admin, action) {
|
|
13164
|
+
if (action === AdminPermission.CREATE) return admin.canCreate;
|
|
13165
|
+
if (action === AdminPermission.EDIT) return admin.canEdit;
|
|
13166
|
+
if (action === AdminPermission.DELETE) return admin.canDelete;
|
|
13167
|
+
return true;
|
|
13168
|
+
}
|
|
13169
|
+
function stampActor(admin, data, actorId, creating) {
|
|
13170
|
+
const columns = adminColumns(admin.model);
|
|
13171
|
+
if (creating && "createdBy" in columns) data.createdBy = actorId;
|
|
13172
|
+
if ("updatedBy" in columns) data.updatedBy = actorId;
|
|
13173
|
+
}
|
|
13174
|
+
function formatMetric(value) {
|
|
13175
|
+
if (typeof value === "string") return value;
|
|
13176
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
13177
|
+
}
|
|
13178
|
+
async function computeBusinessCard(card, dbSession) {
|
|
13179
|
+
const base = {
|
|
13180
|
+
label: card.label,
|
|
13181
|
+
unit: null,
|
|
13182
|
+
direction: "flat",
|
|
13183
|
+
percent: null,
|
|
13184
|
+
previous: "",
|
|
13185
|
+
segments: [],
|
|
13186
|
+
helpText: card.helpText ?? null
|
|
13187
|
+
};
|
|
13188
|
+
let data;
|
|
13189
|
+
try {
|
|
13190
|
+
data = await card.compute(dbSession);
|
|
13191
|
+
} catch (error) {
|
|
13192
|
+
logger2.warning("Admin dashboard card failed", {
|
|
13193
|
+
card: card.label,
|
|
13194
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13195
|
+
});
|
|
13196
|
+
return { ...base, kind: "value", value: "", error: "Could not compute this metric." };
|
|
13197
|
+
}
|
|
13198
|
+
if (data.kind === "partition") {
|
|
13199
|
+
const total = partitionTotal(data);
|
|
13200
|
+
return {
|
|
13201
|
+
...base,
|
|
13202
|
+
kind: "partition",
|
|
13203
|
+
value: formatMetric(total),
|
|
13204
|
+
segments: data.segments.map((segment) => ({
|
|
13205
|
+
label: segment.label,
|
|
13206
|
+
value: formatMetric(segment.value),
|
|
13207
|
+
percent: total === 0 ? 0 : segment.value / total * 100
|
|
13208
|
+
})),
|
|
13209
|
+
error: null
|
|
13210
|
+
};
|
|
13211
|
+
}
|
|
13212
|
+
if (data.kind === "trend") {
|
|
13213
|
+
const percent = trendPercent(data);
|
|
13214
|
+
return {
|
|
13215
|
+
...base,
|
|
13216
|
+
kind: "trend",
|
|
13217
|
+
value: formatMetric(data.value),
|
|
13218
|
+
unit: data.unit ?? null,
|
|
13219
|
+
direction: trendDirection(data),
|
|
13220
|
+
percent: percent === null ? null : `${percent >= 0 ? "+" : ""}${percent.toFixed(1)}%`,
|
|
13221
|
+
previous: formatMetric(data.previous),
|
|
13222
|
+
error: null
|
|
13223
|
+
};
|
|
13224
|
+
}
|
|
13225
|
+
return {
|
|
13226
|
+
...base,
|
|
13227
|
+
kind: "value",
|
|
13228
|
+
value: formatMetric(data.value),
|
|
13229
|
+
unit: data.unit ?? null,
|
|
13230
|
+
error: null
|
|
13231
|
+
};
|
|
13232
|
+
}
|
|
12894
13233
|
function bulkActionsFor(admin) {
|
|
12895
13234
|
const actions = [];
|
|
12896
13235
|
const hasActiveFlag = "isActive" in adminColumns(admin.model);
|
|
@@ -14943,7 +15282,7 @@ async function withTestDatabase(models, fn) {
|
|
|
14943
15282
|
}
|
|
14944
15283
|
|
|
14945
15284
|
// src/version.ts
|
|
14946
|
-
var VERSION = "0.
|
|
15285
|
+
var VERSION = "0.26.0";
|
|
14947
15286
|
|
|
14948
15287
|
Object.defineProperty(exports, "OpenAPIRegistry", {
|
|
14949
15288
|
enumerable: true,
|
|
@@ -15101,6 +15440,7 @@ exports.ADMIN_CSS = ADMIN_CSS;
|
|
|
15101
15440
|
exports.ActivationService = ActivationService;
|
|
15102
15441
|
exports.AdminJsonSite = AdminJsonSite;
|
|
15103
15442
|
exports.AdminModel = AdminModel;
|
|
15443
|
+
exports.AdminPermission = AdminPermission;
|
|
15104
15444
|
exports.AdminSessionStore = AdminSessionStore;
|
|
15105
15445
|
exports.AdminSite = AdminSite;
|
|
15106
15446
|
exports.AppException = AppException;
|
|
@@ -15201,6 +15541,7 @@ exports.activationSchema = activationSchema;
|
|
|
15201
15541
|
exports.addLogSink = addLogSink;
|
|
15202
15542
|
exports.adminAction = adminAction;
|
|
15203
15543
|
exports.adminColumns = adminColumns;
|
|
15544
|
+
exports.adminLens = adminLens;
|
|
15204
15545
|
exports.adminThemeCss = adminThemeCss;
|
|
15205
15546
|
exports.attachWebSocketHub = attachWebSocketHub;
|
|
15206
15547
|
exports.authResponseSchema = authResponseSchema;
|
|
@@ -15302,6 +15643,7 @@ exports.makeToolSpecRouter = makeToolSpecRouter;
|
|
|
15302
15643
|
exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
|
|
15303
15644
|
exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
|
|
15304
15645
|
exports.makeWhatsAppWebhookRouter = makeWhatsAppWebhookRouter;
|
|
15646
|
+
exports.metricCard = metricCard;
|
|
15305
15647
|
exports.mfaChallengeSchema = mfaChallengeSchema;
|
|
15306
15648
|
exports.mfaCodeSchema = mfaCodeSchema;
|
|
15307
15649
|
exports.mfaEnrollResponseSchema = mfaEnrollResponseSchema;
|
|
@@ -15326,6 +15668,7 @@ exports.paginationSchema = paginationSchema;
|
|
|
15326
15668
|
exports.parseAcceptLanguage = parseAcceptLanguage;
|
|
15327
15669
|
exports.parseCookies = parseCookies;
|
|
15328
15670
|
exports.parseFormBody = parseFormBody;
|
|
15671
|
+
exports.partitionTotal = partitionTotal;
|
|
15329
15672
|
exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
|
|
15330
15673
|
exports.passwordResetRequestSchema = passwordResetRequestSchema;
|
|
15331
15674
|
exports.percentField = percentField;
|
|
@@ -15378,6 +15721,8 @@ exports.toUtc = toUtc;
|
|
|
15378
15721
|
exports.tokenFromUrl = tokenFromUrl;
|
|
15379
15722
|
exports.tokenPairSchema = tokenPairSchema;
|
|
15380
15723
|
exports.tokenSettingsShape = tokenSettingsShape;
|
|
15724
|
+
exports.trendDirection = trendDirection;
|
|
15725
|
+
exports.trendPercent = trendPercent;
|
|
15381
15726
|
exports.ufField = ufField;
|
|
15382
15727
|
exports.updatedByColumn = updatedByColumn;
|
|
15383
15728
|
exports.uploadSettingsShape = uploadSettingsShape;
|