@adminforth/dashboard 1.7.0 → 1.9.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 +81 -55
- package/custom/model/dashboard.types.ts +17 -9
- package/custom/skills/adminforth-dashboard/SKILL.md +28 -12
- package/dist/custom/model/dashboard.types.d.ts +15 -8
- package/dist/custom/model/dashboard.types.ts +17 -9
- package/dist/custom/queries/useDashboardConfig.d.ts +222 -4
- package/dist/custom/queries/useWidgetData.d.ts +222 -4
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +28 -12
- package/dist/schema/api.d.ts +5440 -941
- package/dist/schema/api.js +2 -2
- package/dist/schema/widget.d.ts +432 -23
- package/dist/schema/widget.js +1 -1
- package/dist/schema/widgets/charts.d.ts +558 -28
- package/dist/schema/widgets/charts.js +2 -2
- package/dist/schema/widgets/common.d.ts +17 -6
- package/dist/schema/widgets/common.js +16 -7
- package/dist/schema/widgets/gauge-card.d.ts +38 -2
- package/dist/schema/widgets/kpi-card.d.ts +38 -2
- package/dist/schema/widgets/pivot-table.d.ts +38 -2
- package/dist/schema/widgets/table.d.ts +38 -2
- package/dist/services/calc-evaluator.d.ts +1 -0
- package/dist/services/calc-evaluator.js +28 -0
- package/dist/services/dashboardFilterService.d.ts +5 -0
- package/dist/services/dashboardFilterService.js +125 -0
- package/dist/services/widgetDataService.js +53 -201
- package/package.json +2 -1
- package/schema/api.ts +1 -2
- package/schema/widget.ts +0 -1
- package/schema/widgets/charts.ts +1 -2
- package/schema/widgets/common.ts +16 -7
- package/services/calc-evaluator.ts +33 -0
- package/services/dashboardFilterService.ts +162 -0
- package/services/widgetDataService.ts +88 -263
|
@@ -7,33 +7,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
14
|
-
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
|
|
15
|
-
const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
|
|
16
|
-
const FILTER_OPERATORS = {
|
|
17
|
-
eq: Filters.EQ,
|
|
18
|
-
neq: Filters.NEQ,
|
|
19
|
-
gt: Filters.GT,
|
|
20
|
-
gte: Filters.GTE,
|
|
21
|
-
lt: Filters.LT,
|
|
22
|
-
lte: Filters.LTE,
|
|
23
|
-
in: Filters.IN,
|
|
24
|
-
not_in: Filters.NOT_IN,
|
|
25
|
-
like: Filters.LIKE,
|
|
26
|
-
ilike: Filters.ILIKE,
|
|
27
|
-
};
|
|
10
|
+
import { Sorts } from 'adminforth';
|
|
11
|
+
import { getAdminForthFilters, mergeFilters, } from './dashboardFilterService.js';
|
|
12
|
+
import { evaluateCalc } from './calc-evaluator.js';
|
|
28
13
|
export function getWidgetData(adminforth_1, widget_1) {
|
|
29
14
|
return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
|
|
30
|
-
var _a
|
|
15
|
+
var _a;
|
|
31
16
|
if (!('query' in widget)) {
|
|
32
17
|
return null;
|
|
33
18
|
}
|
|
34
|
-
const data =
|
|
35
|
-
? yield getFunnelWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {})
|
|
36
|
-
: yield getQueryWidgetData(adminforth, widget.query, (_b = options.variables) !== null && _b !== void 0 ? _b : {});
|
|
19
|
+
const data = yield getQueryWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {});
|
|
37
20
|
if (widget.target !== 'table' || !options.pagination) {
|
|
38
21
|
return data;
|
|
39
22
|
}
|
|
@@ -48,40 +31,15 @@ export function getWidgetData(adminforth_1, widget_1) {
|
|
|
48
31
|
} });
|
|
49
32
|
});
|
|
50
33
|
}
|
|
51
|
-
function getFunnelWidgetData(adminforth, query, variables) {
|
|
52
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
-
var _a;
|
|
54
|
-
const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
|
|
55
|
-
var _a, _b;
|
|
56
|
-
const valueField = step.metric.as;
|
|
57
|
-
const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, [step.metric], []);
|
|
58
|
-
const row = {
|
|
59
|
-
name: step.name,
|
|
60
|
-
resource: step.resource,
|
|
61
|
-
[valueField]: (_a = values[valueField]) !== null && _a !== void 0 ? _a : 0,
|
|
62
|
-
};
|
|
63
|
-
for (const calc of (_b = query.calcs) !== null && _b !== void 0 ? _b : []) {
|
|
64
|
-
row[calc.as] = evaluateCalc(calc.calc, row, variables);
|
|
65
|
-
}
|
|
66
|
-
return row;
|
|
67
|
-
})));
|
|
68
|
-
return {
|
|
69
|
-
kind: 'aggregate',
|
|
70
|
-
columns: [
|
|
71
|
-
'name',
|
|
72
|
-
...Array.from(new Set(query.steps.map((step) => step.metric.as))),
|
|
73
|
-
...Array.from(new Set(((_a = query.calcs) !== null && _a !== void 0 ? _a : []).map((calc) => calc.as))),
|
|
74
|
-
],
|
|
75
|
-
rows,
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
34
|
function getQueryWidgetData(adminforth, query, variables) {
|
|
80
35
|
return __awaiter(this, void 0, void 0, function* () {
|
|
81
36
|
var _a, _b, _c;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
37
|
+
if (isStepsQuery(query)) {
|
|
38
|
+
return getStepsQueryData(adminforth, query, variables);
|
|
39
|
+
}
|
|
40
|
+
const singleAggregateSelect = getSingleAggregateSelectItem(query);
|
|
41
|
+
if (singleAggregateSelect) {
|
|
42
|
+
return getSingleAggregateWidgetData(adminforth, query, singleAggregateSelect);
|
|
85
43
|
}
|
|
86
44
|
const selectedRows = isAggregateQuery(query)
|
|
87
45
|
? yield buildAggregateQueryRows(adminforth, query, variables)
|
|
@@ -102,18 +60,44 @@ function getQueryWidgetData(adminforth, query, variables) {
|
|
|
102
60
|
};
|
|
103
61
|
});
|
|
104
62
|
}
|
|
105
|
-
function
|
|
63
|
+
function getStepsQueryData(adminforth, query, variables) {
|
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
var _a, _b, _c, _d;
|
|
66
|
+
const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
const select = getStepSelect(step);
|
|
68
|
+
const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, select, []);
|
|
69
|
+
const row = buildCalculatedRow(Object.assign({ name: step.name, resource: step.resource }, values), select, query.calcs, variables);
|
|
70
|
+
return row;
|
|
71
|
+
})));
|
|
72
|
+
const orderedRows = sortRows(rows, query.order_by);
|
|
73
|
+
const slicedRows = typeof query.limit === 'number'
|
|
74
|
+
? orderedRows.slice((_a = query.offset) !== null && _a !== void 0 ? _a : 0, ((_b = query.offset) !== null && _b !== void 0 ? _b : 0) + query.limit)
|
|
75
|
+
: orderedRows.slice((_c = query.offset) !== null && _c !== void 0 ? _c : 0);
|
|
76
|
+
const columns = Array.from(new Set([
|
|
77
|
+
'name',
|
|
78
|
+
'resource',
|
|
79
|
+
...query.steps.flatMap((step) => getStepSelect(step).map((item) => item.as)),
|
|
80
|
+
...((_d = query.calcs) !== null && _d !== void 0 ? _d : []).map((item) => item.as),
|
|
81
|
+
]));
|
|
82
|
+
return {
|
|
83
|
+
kind: 'aggregate',
|
|
84
|
+
columns,
|
|
85
|
+
rows: slicedRows,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function getSingleAggregateWidgetData(adminforth, query, aggregate) {
|
|
106
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
107
91
|
var _a;
|
|
108
|
-
const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [
|
|
92
|
+
const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [aggregate], []);
|
|
109
93
|
const values = {
|
|
110
|
-
[
|
|
94
|
+
[aggregate.as]: (_a = currentValues[aggregate.as]) !== null && _a !== void 0 ? _a : 0,
|
|
111
95
|
};
|
|
112
96
|
const rows = query.sparkline
|
|
113
|
-
? yield
|
|
97
|
+
? yield getSingleAggregateSparklineRows(adminforth, query, aggregate, getAdminForthFilters(query.filters))
|
|
114
98
|
: [values];
|
|
115
99
|
const columns = Array.from(new Set([
|
|
116
|
-
|
|
100
|
+
aggregate.as,
|
|
117
101
|
...(query.sparkline ? [query.sparkline.as] : []),
|
|
118
102
|
]));
|
|
119
103
|
return {
|
|
@@ -124,7 +108,7 @@ function getMetricWidgetData(adminforth, query, metric) {
|
|
|
124
108
|
};
|
|
125
109
|
});
|
|
126
110
|
}
|
|
127
|
-
function
|
|
111
|
+
function getSingleAggregateSparklineRows(adminforth, query, aggregate, filters) {
|
|
128
112
|
return __awaiter(this, void 0, void 0, function* () {
|
|
129
113
|
const sparkline = query.sparkline;
|
|
130
114
|
const groupBy = [{
|
|
@@ -132,7 +116,7 @@ function getMetricSparklineRows(adminforth, query, metric, filters) {
|
|
|
132
116
|
as: sparkline.as,
|
|
133
117
|
grain: sparkline.grain,
|
|
134
118
|
}];
|
|
135
|
-
const rows = yield getAggregateRows(adminforth, query.resource, filters, [
|
|
119
|
+
const rows = yield getAggregateRows(adminforth, query.resource, filters, [aggregate], groupBy);
|
|
136
120
|
return rows.map((row) => {
|
|
137
121
|
var _a;
|
|
138
122
|
return (Object.assign(Object.assign({}, (_a = query.sparkline) === null || _a === void 0 ? void 0 : _a.fill_missing), row));
|
|
@@ -191,7 +175,7 @@ function getAggregateRows(adminforth, resourceId, baseFilters, select, groupBy)
|
|
|
191
175
|
function buildCalculatedRow(baseValues, select, calcs = [], variables) {
|
|
192
176
|
const values = Object.assign({}, baseValues);
|
|
193
177
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
194
|
-
values[item.as] = evaluateCalc(item.calc, values
|
|
178
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
195
179
|
}
|
|
196
180
|
return values;
|
|
197
181
|
}
|
|
@@ -206,33 +190,10 @@ function buildPlainRow(row, select, calcs = [], variables) {
|
|
|
206
190
|
}
|
|
207
191
|
}
|
|
208
192
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
209
|
-
values[item.as] = evaluateCalc(item.calc, values
|
|
193
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
210
194
|
}
|
|
211
195
|
return values;
|
|
212
196
|
}
|
|
213
|
-
function evaluateCalc(calc, values, variables) {
|
|
214
|
-
const expression = calc
|
|
215
|
-
.replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
|
|
216
|
-
var _a;
|
|
217
|
-
const map = resolveVariablePath(variables, path);
|
|
218
|
-
const key = String((_a = values[keyField]) !== null && _a !== void 0 ? _a : '');
|
|
219
|
-
return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
|
|
220
|
-
? map[key]
|
|
221
|
-
: Number(defaultValue)));
|
|
222
|
-
})
|
|
223
|
-
.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
224
|
-
if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
|
|
225
|
-
throw new Error(`Unsupported calc expression: ${calc}`);
|
|
226
|
-
}
|
|
227
|
-
return Function(`"use strict"; return (${expression});`)();
|
|
228
|
-
}
|
|
229
|
-
function resolveVariablePath(variables, path) {
|
|
230
|
-
return path
|
|
231
|
-
.replace(VARIABLE_PATH_PREFIX_RE, '')
|
|
232
|
-
.split('.')
|
|
233
|
-
.filter(Boolean)
|
|
234
|
-
.reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
|
|
235
|
-
}
|
|
236
197
|
function sortRows(rows, orderBy = []) {
|
|
237
198
|
if (!orderBy.length) {
|
|
238
199
|
return rows;
|
|
@@ -280,7 +241,7 @@ function isAggregateQuery(query) {
|
|
|
280
241
|
return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
|
|
281
242
|
|| ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
|
|
282
243
|
}
|
|
283
|
-
function
|
|
244
|
+
function getSingleAggregateSelectItem(query) {
|
|
284
245
|
var _a, _b;
|
|
285
246
|
if ((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length) {
|
|
286
247
|
return undefined;
|
|
@@ -292,6 +253,12 @@ function getSingleAggregateMetricSelect(query) {
|
|
|
292
253
|
}
|
|
293
254
|
return aggregateItems[0];
|
|
294
255
|
}
|
|
256
|
+
function isStepsQuery(query) {
|
|
257
|
+
return query.source === 'steps';
|
|
258
|
+
}
|
|
259
|
+
function getStepSelect(step) {
|
|
260
|
+
return step.select;
|
|
261
|
+
}
|
|
295
262
|
function isFieldSelectItem(item) {
|
|
296
263
|
return 'field' in item && !('agg' in item);
|
|
297
264
|
}
|
|
@@ -408,23 +375,6 @@ function getFilterCacheKey(filters) {
|
|
|
408
375
|
}
|
|
409
376
|
return JSON.stringify(filters);
|
|
410
377
|
}
|
|
411
|
-
function mergeFilters(...filters) {
|
|
412
|
-
const merged = [];
|
|
413
|
-
for (const filter of filters) {
|
|
414
|
-
const normalized = getAdminForthFilters(filter);
|
|
415
|
-
if (Array.isArray(normalized)) {
|
|
416
|
-
merged.push(...normalized);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (normalized) {
|
|
420
|
-
merged.push(normalized);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!merged.length) {
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
return merged.length === 1 ? merged[0] : merged;
|
|
427
|
-
}
|
|
428
378
|
function getHiddenAggregateAlias(groupBy, select) {
|
|
429
379
|
const usedAliases = new Set([
|
|
430
380
|
...groupBy.map((item) => item.as),
|
|
@@ -462,104 +412,6 @@ function formatGroupValue(value, grain) {
|
|
|
462
412
|
}
|
|
463
413
|
return value;
|
|
464
414
|
}
|
|
465
|
-
function getAdminForthFilters(filters) {
|
|
466
|
-
if (Array.isArray(filters)) {
|
|
467
|
-
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
468
|
-
? toAdminForthFilter(filter)
|
|
469
|
-
: filter);
|
|
470
|
-
}
|
|
471
|
-
if (isDashboardFilterExpression(filters)) {
|
|
472
|
-
return toAdminForthFilter(filters);
|
|
473
|
-
}
|
|
474
|
-
if (filters) {
|
|
475
|
-
return filters;
|
|
476
|
-
}
|
|
477
|
-
return [];
|
|
478
|
-
}
|
|
479
|
-
function isDashboardFilterExpression(value) {
|
|
480
|
-
if (Array.isArray(value)) {
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
if (!isRecord(value)) {
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
return 'and' in value
|
|
487
|
-
|| 'or' in value
|
|
488
|
-
|| 'eq' in value
|
|
489
|
-
|| 'neq' in value
|
|
490
|
-
|| 'gt' in value
|
|
491
|
-
|| 'gte' in value
|
|
492
|
-
|| 'lt' in value
|
|
493
|
-
|| 'lte' in value
|
|
494
|
-
|| 'in' in value
|
|
495
|
-
|| 'not_in' in value
|
|
496
|
-
|| 'like' in value
|
|
497
|
-
|| 'ilike' in value;
|
|
498
|
-
}
|
|
499
|
-
function toAdminForthFilter(filter) {
|
|
500
|
-
if (Array.isArray(filter)) {
|
|
501
|
-
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
502
|
-
}
|
|
503
|
-
if ('and' in filter) {
|
|
504
|
-
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
505
|
-
}
|
|
506
|
-
if ('or' in filter) {
|
|
507
|
-
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
508
|
-
}
|
|
509
|
-
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
510
|
-
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
511
|
-
return createFilter(filter.field, resolveFilterValue(filter[operator]));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return Filters.AND([]);
|
|
515
|
-
}
|
|
516
|
-
function resolveFilterValue(value) {
|
|
517
|
-
if (Array.isArray(value)) {
|
|
518
|
-
return value.map((item) => resolveFilterValue(item));
|
|
519
|
-
}
|
|
520
|
-
if (!isRecord(value)) {
|
|
521
|
-
return value;
|
|
522
|
-
}
|
|
523
|
-
if (value.now === true) {
|
|
524
|
-
return new Date().toISOString();
|
|
525
|
-
}
|
|
526
|
-
if (typeof value.now_minus === 'string') {
|
|
527
|
-
return subtractDuration(new Date(), value.now_minus).toISOString();
|
|
528
|
-
}
|
|
529
|
-
return value;
|
|
530
|
-
}
|
|
531
|
-
function subtractDuration(now, duration) {
|
|
532
|
-
const match = duration.match(RELATIVE_DURATION_RE);
|
|
533
|
-
if (!match) {
|
|
534
|
-
throw new Error(`Unsupported relative date duration: ${duration}`);
|
|
535
|
-
}
|
|
536
|
-
const amount = Number(match[1]);
|
|
537
|
-
const unit = match[2];
|
|
538
|
-
const date = new Date(now);
|
|
539
|
-
if (unit === 'h') {
|
|
540
|
-
date.setUTCHours(date.getUTCHours() - amount);
|
|
541
|
-
}
|
|
542
|
-
else if (unit === 'd') {
|
|
543
|
-
date.setUTCDate(date.getUTCDate() - amount);
|
|
544
|
-
}
|
|
545
|
-
else if (unit === 'w') {
|
|
546
|
-
date.setUTCDate(date.getUTCDate() - amount * 7);
|
|
547
|
-
}
|
|
548
|
-
else if (unit === 'mo') {
|
|
549
|
-
date.setUTCMonth(date.getUTCMonth() - amount);
|
|
550
|
-
}
|
|
551
|
-
else if (unit === 'y') {
|
|
552
|
-
date.setUTCFullYear(date.getUTCFullYear() - amount);
|
|
553
|
-
}
|
|
554
|
-
return date;
|
|
555
|
-
}
|
|
556
|
-
function toFiniteNumber(value) {
|
|
557
|
-
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
558
|
-
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
559
|
-
}
|
|
560
|
-
function isRecord(value) {
|
|
561
|
-
return typeof value === 'object' && value !== null;
|
|
562
|
-
}
|
|
563
415
|
export function createWidgetDataService(adminforth) {
|
|
564
416
|
return {
|
|
565
417
|
getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/dashboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
}
|
|
57
57
|
],
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"expr-eval-fork": "^3.0.3",
|
|
59
60
|
"vue": "^3.5.34",
|
|
60
61
|
"vue-router": "^5.0.7",
|
|
61
62
|
"yaml": "^2.9.0",
|
package/schema/api.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { toJSONSchema, z } from 'zod'
|
|
|
2
2
|
import {
|
|
3
3
|
BarChartSchema,
|
|
4
4
|
FunnelChartSchema,
|
|
5
|
-
FunnelQueryConfigSchema,
|
|
6
5
|
GaugeCardViewConfigSchema,
|
|
7
6
|
HistogramChartSchema,
|
|
8
7
|
KpiCardViewConfigSchema,
|
|
@@ -153,7 +152,7 @@ const ConfigurableHistogramChartWidgetConfigSchema = WidgetEditableBaseSchema.ex
|
|
|
153
152
|
const ConfigurableFunnelChartWidgetConfigSchema = WidgetEditableBaseSchema.extend({
|
|
154
153
|
target: z.literal('chart'),
|
|
155
154
|
chart: FunnelChartSchema,
|
|
156
|
-
query:
|
|
155
|
+
query: QueryConfigSchema,
|
|
157
156
|
})
|
|
158
157
|
|
|
159
158
|
const ConfigurablePivotTableWidgetConfigSchema = WidgetEditableBaseSchema.extend({
|
package/schema/widget.ts
CHANGED
package/schema/widgets/charts.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import {
|
|
3
3
|
ChartFieldRefSchema,
|
|
4
|
-
FunnelQueryConfigSchema,
|
|
5
4
|
QueryConfigSchema,
|
|
6
5
|
WidgetBaseSchema,
|
|
7
6
|
} from './common.js'
|
|
@@ -100,7 +99,7 @@ export const HistogramChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
|
100
99
|
export const FunnelChartWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
101
100
|
target: z.literal('chart'),
|
|
102
101
|
chart: FunnelChartSchema,
|
|
103
|
-
query:
|
|
102
|
+
query: QueryConfigSchema,
|
|
104
103
|
})
|
|
105
104
|
|
|
106
105
|
export const ChartWidgetTargetConfigSchema = z.union([
|
package/schema/widgets/common.ts
CHANGED
|
@@ -140,7 +140,8 @@ export const QueryCalcItemSchema = z.object({
|
|
|
140
140
|
as: z.string(),
|
|
141
141
|
}).strict()
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
const ResourceQueryConfigSchema = z.object({
|
|
144
|
+
source: z.literal('resource').optional(),
|
|
144
145
|
resource: z.string(),
|
|
145
146
|
select: z.array(QuerySelectItemSchema).optional(),
|
|
146
147
|
sparkline: z.object({
|
|
@@ -159,17 +160,25 @@ export const QueryConfigSchema = z.object({
|
|
|
159
160
|
formatting: z.record(z.string(), z.unknown()).optional(),
|
|
160
161
|
}).strict()
|
|
161
162
|
|
|
162
|
-
const
|
|
163
|
+
const StepsQuerySelectStepSchema = z.object({
|
|
163
164
|
name: z.string(),
|
|
164
165
|
resource: z.string(),
|
|
165
|
-
|
|
166
|
+
select: z.array(QueryAggregateSelectItemSchema).min(1),
|
|
166
167
|
filters: FilterExpressionSchema.optional(),
|
|
167
168
|
}).strict()
|
|
168
169
|
|
|
169
|
-
export const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
export const QueryConfigSchema = z.union([
|
|
171
|
+
ResourceQueryConfigSchema,
|
|
172
|
+
z.object({
|
|
173
|
+
source: z.literal('steps'),
|
|
174
|
+
steps: z.array(StepsQuerySelectStepSchema).min(1),
|
|
175
|
+
calcs: z.array(QueryCalcItemSchema).optional(),
|
|
176
|
+
order_by: z.array(QueryOrderByItemSchema).optional(),
|
|
177
|
+
limit: z.number().int().positive().optional(),
|
|
178
|
+
offset: z.number().int().nonnegative().optional(),
|
|
179
|
+
formatting: z.record(z.string(), z.unknown()).optional(),
|
|
180
|
+
}).strict(),
|
|
181
|
+
])
|
|
173
182
|
|
|
174
183
|
export const WidgetPersistedFieldsSchema = z.object({
|
|
175
184
|
id: z.string(),
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Parser } from 'expr-eval-fork';
|
|
2
|
+
|
|
3
|
+
const CALC_PARSER_OPTIONS = {
|
|
4
|
+
allowMemberAccess: false,
|
|
5
|
+
operators: {
|
|
6
|
+
assignment: false,
|
|
7
|
+
concatenate: false,
|
|
8
|
+
conditional: true,
|
|
9
|
+
comparison: true,
|
|
10
|
+
fndef: false,
|
|
11
|
+
in: false,
|
|
12
|
+
logical: true,
|
|
13
|
+
random: false,
|
|
14
|
+
},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const CALC_PARSER = new Parser(CALC_PARSER_OPTIONS);
|
|
18
|
+
|
|
19
|
+
export function evaluateCalc(calc: string, values: Record<string, unknown>) {
|
|
20
|
+
return CALC_PARSER.parse(calc).evaluate(normalizeCalcValues(values));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCalcValues(values: Record<string, unknown>) {
|
|
24
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
25
|
+
key,
|
|
26
|
+
typeof value === 'string' ? value : toFiniteNumber(value),
|
|
27
|
+
]));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toFiniteNumber(value: unknown) {
|
|
31
|
+
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
32
|
+
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Filters } from 'adminforth';
|
|
2
|
+
import type {
|
|
3
|
+
IAdminForthAndOrFilter,
|
|
4
|
+
IAdminForthSingleFilter,
|
|
5
|
+
} from 'adminforth';
|
|
6
|
+
import type { FilterExpression } from '../custom/model/dashboard.types.js';
|
|
7
|
+
|
|
8
|
+
export type DashboardQueryFilters =
|
|
9
|
+
| IAdminForthSingleFilter
|
|
10
|
+
| IAdminForthAndOrFilter
|
|
11
|
+
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
12
|
+
|
|
13
|
+
const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
|
|
14
|
+
|
|
15
|
+
const FILTER_OPERATORS = {
|
|
16
|
+
eq: Filters.EQ,
|
|
17
|
+
neq: Filters.NEQ,
|
|
18
|
+
gt: Filters.GT,
|
|
19
|
+
gte: Filters.GTE,
|
|
20
|
+
lt: Filters.LT,
|
|
21
|
+
lte: Filters.LTE,
|
|
22
|
+
in: Filters.IN,
|
|
23
|
+
not_in: Filters.NOT_IN,
|
|
24
|
+
like: Filters.LIKE,
|
|
25
|
+
ilike: Filters.ILIKE,
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export function getAdminForthFilters(filters: FilterExpression | DashboardQueryFilters | undefined): DashboardQueryFilters {
|
|
29
|
+
if (Array.isArray(filters)) {
|
|
30
|
+
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
31
|
+
? toAdminForthFilter(filter)
|
|
32
|
+
: filter);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isDashboardFilterExpression(filters)) {
|
|
36
|
+
return toAdminForthFilter(filters);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (filters) {
|
|
40
|
+
return filters;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function mergeFilters(...filters: Array<FilterExpression | DashboardQueryFilters | undefined>) {
|
|
47
|
+
const merged: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> = [];
|
|
48
|
+
|
|
49
|
+
for (const filter of filters) {
|
|
50
|
+
const normalized = getAdminForthFilters(filter);
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(normalized)) {
|
|
53
|
+
merged.push(...normalized);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (normalized) {
|
|
58
|
+
merged.push(normalized);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!merged.length) {
|
|
63
|
+
return [] as DashboardQueryFilters;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isDashboardFilterExpression(value: unknown): value is FilterExpression {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isRecord(value)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return 'and' in value
|
|
79
|
+
|| 'or' in value
|
|
80
|
+
|| 'eq' in value
|
|
81
|
+
|| 'neq' in value
|
|
82
|
+
|| 'gt' in value
|
|
83
|
+
|| 'gte' in value
|
|
84
|
+
|| 'lt' in value
|
|
85
|
+
|| 'lte' in value
|
|
86
|
+
|| 'in' in value
|
|
87
|
+
|| 'not_in' in value
|
|
88
|
+
|| 'like' in value
|
|
89
|
+
|| 'ilike' in value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
93
|
+
if (Array.isArray(filter)) {
|
|
94
|
+
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ('and' in filter) {
|
|
98
|
+
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if ('or' in filter) {
|
|
102
|
+
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
107
|
+
return createFilter(filter.field, resolveFilterValue(filter[operator as keyof typeof FILTER_OPERATORS]));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Filters.AND([]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveFilterValue(value: unknown): unknown {
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return value.map((item) => resolveFilterValue(item));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (value.now === true) {
|
|
124
|
+
return new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof value.now_minus === 'string') {
|
|
128
|
+
return subtractDuration(new Date(), value.now_minus).toISOString();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function subtractDuration(now: Date, duration: string) {
|
|
135
|
+
const match = duration.match(RELATIVE_DURATION_RE);
|
|
136
|
+
|
|
137
|
+
if (!match) {
|
|
138
|
+
throw new Error(`Unsupported relative date duration: ${duration}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const amount = Number(match[1]);
|
|
142
|
+
const unit = match[2];
|
|
143
|
+
const date = new Date(now);
|
|
144
|
+
|
|
145
|
+
if (unit === 'h') {
|
|
146
|
+
date.setUTCHours(date.getUTCHours() - amount);
|
|
147
|
+
} else if (unit === 'd') {
|
|
148
|
+
date.setUTCDate(date.getUTCDate() - amount);
|
|
149
|
+
} else if (unit === 'w') {
|
|
150
|
+
date.setUTCDate(date.getUTCDate() - amount * 7);
|
|
151
|
+
} else if (unit === 'mo') {
|
|
152
|
+
date.setUTCMonth(date.getUTCMonth() - amount);
|
|
153
|
+
} else if (unit === 'y') {
|
|
154
|
+
date.setUTCFullYear(date.getUTCFullYear() - amount);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return date;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
161
|
+
return typeof value === 'object' && value !== null;
|
|
162
|
+
}
|