@adminforth/dashboard 1.3.0 → 1.4.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 +103 -15
- package/custom/api/dashboardApi.ts +9 -8
- package/custom/model/dashboard.types.ts +63 -270
- package/custom/model/dashboardTopics.ts +5 -0
- package/custom/runtime/DashboardGroup.vue +2 -2
- package/custom/runtime/DashboardPage.vue +17 -7
- package/custom/runtime/DashboardRuntime.vue +20 -8
- package/custom/runtime/WidgetRenderer.vue +1 -2
- package/custom/runtime/WidgetShell.vue +3 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
- package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/api/dashboardApi.d.ts +4 -7
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +9 -8
- package/dist/custom/model/dashboard.types.d.ts +40 -31
- package/dist/custom/model/dashboard.types.js +13 -152
- package/dist/custom/model/dashboard.types.ts +63 -270
- package/dist/custom/model/dashboardTopics.d.ts +2 -0
- package/dist/custom/model/dashboardTopics.js +8 -0
- package/dist/custom/model/dashboardTopics.ts +5 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +116 -96
- package/dist/custom/queries/useWidgetData.d.ts +116 -96
- package/dist/custom/runtime/DashboardGroup.vue +2 -2
- package/dist/custom/runtime/DashboardPage.vue +17 -7
- package/dist/custom/runtime/DashboardRuntime.vue +20 -8
- package/dist/custom/runtime/WidgetRenderer.vue +1 -2
- package/dist/custom/runtime/WidgetShell.vue +3 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
- package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
- package/dist/custom/widgets/chart/chart.types.js +0 -23
- package/dist/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/endpoint/dashboard.d.ts +6 -2
- package/dist/endpoint/dashboard.js +29 -5
- package/dist/endpoint/groups.d.ts +2 -21
- package/dist/endpoint/groups.js +18 -16
- package/dist/endpoint/widgets.d.ts +2 -4
- package/dist/endpoint/widgets.js +28 -74
- package/dist/index.js +1 -3
- package/dist/schema/api.d.ts +2172 -500
- package/dist/schema/api.js +21 -13
- package/dist/schema/widget.d.ts +1076 -263
- package/dist/schema/widget.js +108 -49
- package/dist/services/dashboardConfigService.d.ts +0 -10
- package/dist/services/dashboardConfigService.js +6 -21
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +266 -206
- package/endpoint/dashboard.ts +47 -7
- package/endpoint/groups.ts +25 -42
- package/endpoint/widgets.ts +41 -96
- package/index.ts +0 -3
- package/package.json +3 -3
- package/schema/api.ts +23 -13
- package/schema/widget.ts +119 -55
- package/services/dashboardConfigService.ts +6 -25
- package/services/widgetDataService.ts +350 -237
- package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/services/widgetConfigValidator.d.ts +0 -8
- package/dist/services/widgetConfigValidator.js +0 -27
- package/services/widgetConfigValidator.ts +0 -61
|
@@ -8,17 +8,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { Filters, Sorts } from 'adminforth';
|
|
11
|
-
const NOW_MINUS_RE = /^(\d+)([dhw])$/;
|
|
12
11
|
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
12
|
+
const LOOKUP_CALL_RE = /lookup\(\s*(\$variables(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
13
|
+
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
13
14
|
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
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
|
+
};
|
|
14
27
|
export function getWidgetData(adminforth_1, widget_1) {
|
|
15
28
|
return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
|
|
29
|
+
var _a, _b;
|
|
16
30
|
if (!('query' in widget)) {
|
|
17
31
|
return null;
|
|
18
32
|
}
|
|
19
33
|
const data = 'steps' in widget.query
|
|
20
|
-
? yield getFunnelWidgetData(adminforth, widget.query)
|
|
21
|
-
: yield getQueryWidgetData(adminforth, widget.query);
|
|
34
|
+
? yield getFunnelWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {})
|
|
35
|
+
: yield getQueryWidgetData(adminforth, widget.query, (_b = options.variables) !== null && _b !== void 0 ? _b : {});
|
|
22
36
|
if (widget.target !== 'table' || !options.pagination) {
|
|
23
37
|
return data;
|
|
24
38
|
}
|
|
@@ -33,29 +47,41 @@ export function getWidgetData(adminforth_1, widget_1) {
|
|
|
33
47
|
} });
|
|
34
48
|
});
|
|
35
49
|
}
|
|
36
|
-
function getFunnelWidgetData(adminforth, query) {
|
|
50
|
+
function getFunnelWidgetData(adminforth, query, variables) {
|
|
37
51
|
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
var _a;
|
|
38
53
|
const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
|
|
54
|
+
var _a, _b;
|
|
39
55
|
const valueField = step.metric.as;
|
|
40
|
-
const
|
|
41
|
-
|
|
56
|
+
const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, [step.metric], []);
|
|
57
|
+
const row = {
|
|
42
58
|
name: step.name,
|
|
43
|
-
|
|
59
|
+
resource: step.resource,
|
|
60
|
+
[valueField]: (_a = values[valueField]) !== null && _a !== void 0 ? _a : 0,
|
|
44
61
|
};
|
|
62
|
+
for (const calc of (_b = query.calcs) !== null && _b !== void 0 ? _b : []) {
|
|
63
|
+
row[calc.as] = evaluateCalc(calc.calc, row, variables);
|
|
64
|
+
}
|
|
65
|
+
return row;
|
|
45
66
|
})));
|
|
46
67
|
return {
|
|
47
68
|
kind: 'aggregate',
|
|
48
|
-
columns: [
|
|
69
|
+
columns: [
|
|
70
|
+
'name',
|
|
71
|
+
...Array.from(new Set(query.steps.map((step) => step.metric.as))),
|
|
72
|
+
...Array.from(new Set(((_a = query.calcs) !== null && _a !== void 0 ? _a : []).map((calc) => calc.as))),
|
|
73
|
+
],
|
|
49
74
|
rows,
|
|
50
75
|
};
|
|
51
76
|
});
|
|
52
77
|
}
|
|
53
|
-
function getQueryWidgetData(adminforth, query) {
|
|
78
|
+
function getQueryWidgetData(adminforth, query, variables) {
|
|
54
79
|
return __awaiter(this, void 0, void 0, function* () {
|
|
55
80
|
var _a, _b, _c;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
const selectedRows = isAggregateQuery(query)
|
|
82
|
+
? yield buildAggregateQueryRows(adminforth, query, variables)
|
|
83
|
+
: buildPlainQueryRows(yield getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.order_by)), query, variables);
|
|
84
|
+
const orderedRows = sortRows(selectedRows, query.order_by);
|
|
59
85
|
const slicedRows = typeof query.limit === 'number'
|
|
60
86
|
? orderedRows.slice((_a = query.offset) !== null && _a !== void 0 ? _a : 0, ((_b = query.offset) !== null && _b !== void 0 ? _b : 0) + query.limit)
|
|
61
87
|
: orderedRows.slice((_c = query.offset) !== null && _c !== void 0 ? _c : 0);
|
|
@@ -73,58 +99,61 @@ function getQueryWidgetData(adminforth, query) {
|
|
|
73
99
|
}
|
|
74
100
|
function getResourceRows(adminforth, resourceId, filters, sort) {
|
|
75
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
-
return adminforth.resource(resourceId).list(
|
|
102
|
+
return adminforth.resource(resourceId).list(getAdminForthFilters(filters), undefined, 0, sort);
|
|
77
103
|
});
|
|
78
104
|
}
|
|
79
|
-
function
|
|
80
|
-
var _a
|
|
105
|
+
function buildPlainQueryRows(rows, query, variables) {
|
|
106
|
+
var _a;
|
|
81
107
|
const select = (_a = query.select) !== null && _a !== void 0 ? _a : getDefaultSelect(rows);
|
|
82
|
-
|
|
83
|
-
if (isAggregateQuery(query)) {
|
|
84
|
-
return buildGroupedRows(rows, select, groupBy, query.calcs);
|
|
85
|
-
}
|
|
86
|
-
return rows.map((row) => buildPlainRow(row, select, query.calcs));
|
|
108
|
+
return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
|
|
87
109
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
for (const row of rows) {
|
|
99
|
-
const values = Object.fromEntries(effectiveGroupBy.map((item) => {
|
|
100
|
-
const field = getGroupByField(item);
|
|
101
|
-
const alias = getGroupByAlias(item);
|
|
102
|
-
const grain = getGroupByGrain(item);
|
|
103
|
-
return [alias, formatGroupValue(row[field], grain)];
|
|
104
|
-
}));
|
|
105
|
-
const key = JSON.stringify(values);
|
|
106
|
-
const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { values, rows: [] };
|
|
107
|
-
group.rows.push(row);
|
|
108
|
-
groups.set(key, group);
|
|
109
|
-
}
|
|
110
|
-
return Array.from(groups.values()).map((group) => (Object.assign(Object.assign({}, group.values), calculateGroupValues(group.rows, select, calcs))));
|
|
110
|
+
function buildAggregateQueryRows(adminforth, query, variables) {
|
|
111
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
112
|
+
var _a;
|
|
113
|
+
const select = (_a = query.select) !== null && _a !== void 0 ? _a : [];
|
|
114
|
+
const effectiveGroupBy = getEffectiveGroupBy(query.group_by, select);
|
|
115
|
+
const aggregateSelect = select.filter(isAggregateSelectItem);
|
|
116
|
+
const rows = yield getAggregateRows(adminforth, query.resource, query.filters, aggregateSelect, effectiveGroupBy);
|
|
117
|
+
return rows.map((row) => buildCalculatedRow(row, select, query.calcs, variables));
|
|
118
|
+
});
|
|
111
119
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
function getAggregateRows(adminforth, resourceId, baseFilters, select, groupBy) {
|
|
121
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
var _a;
|
|
123
|
+
const resource = adminforth.resource(resourceId);
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
const groupByRules = groupBy.length ? groupBy.map(toAggregateGroupByRule) : undefined;
|
|
126
|
+
const aggregateSelectGroups = groupAggregateSelectItems(select);
|
|
127
|
+
if (groupBy.length) {
|
|
128
|
+
const groupSeedAlias = getHiddenAggregateAlias(groupBy, select);
|
|
129
|
+
const groupSeedRows = yield resource.aggregate(getAdminForthFilters(baseFilters), { [groupSeedAlias]: { operation: 'count' } }, groupByRules);
|
|
130
|
+
for (const row of groupSeedRows) {
|
|
131
|
+
ensureAggregateGroup(groups, row, groupBy);
|
|
132
|
+
}
|
|
120
133
|
}
|
|
121
|
-
|
|
134
|
+
for (const filterGroup of aggregateSelectGroups) {
|
|
135
|
+
const rows = yield resource.aggregate(mergeFilters(baseFilters, filterGroup.filters), Object.fromEntries(filterGroup.items.map((item) => [item.as, toAggregationRule(item)])), groupByRules);
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
const values = ensureAggregateGroup(groups, row, groupBy);
|
|
138
|
+
for (const item of filterGroup.items) {
|
|
139
|
+
values[item.as] = (_a = row[item.as]) !== null && _a !== void 0 ? _a : 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!groups.size && !groupBy.length && select.length) {
|
|
144
|
+
groups.set(JSON.stringify({}), {});
|
|
145
|
+
}
|
|
146
|
+
return Array.from(groups.values(), (row) => applyAggregateDefaults(row, select));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function buildCalculatedRow(baseValues, select, calcs = [], variables) {
|
|
150
|
+
const values = Object.assign({}, baseValues);
|
|
122
151
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
123
|
-
values[item.as] = evaluateCalc(item.calc, values);
|
|
152
|
+
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
124
153
|
}
|
|
125
154
|
return values;
|
|
126
155
|
}
|
|
127
|
-
function buildPlainRow(row, select, calcs = []) {
|
|
156
|
+
function buildPlainRow(row, select, calcs = [], variables) {
|
|
128
157
|
var _a;
|
|
129
158
|
const values = {};
|
|
130
159
|
for (const item of select) {
|
|
@@ -135,52 +164,33 @@ function buildPlainRow(row, select, calcs = []) {
|
|
|
135
164
|
}
|
|
136
165
|
}
|
|
137
166
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
138
|
-
values[item.as] = evaluateCalc(item.calc, values);
|
|
167
|
+
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
139
168
|
}
|
|
140
169
|
return values;
|
|
141
170
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: 0);
|
|
154
|
-
case 'min':
|
|
155
|
-
return aggregateNumbers(rows, item.field, (values) => values.length ? Math.min(...values) : 0);
|
|
156
|
-
case 'max':
|
|
157
|
-
return aggregateNumbers(rows, item.field, (values) => values.length ? Math.max(...values) : 0);
|
|
158
|
-
case 'median':
|
|
159
|
-
return aggregateNumbers(rows, item.field, calculateMedian);
|
|
160
|
-
default:
|
|
161
|
-
throw new Error(`Unsupported aggregation operation: ${item.agg}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function aggregateNumbers(rows, field, aggregate) {
|
|
165
|
-
return aggregate(rows.map((row) => toFiniteNumber(row[field])).filter(Number.isFinite));
|
|
166
|
-
}
|
|
167
|
-
function calculateMedian(values) {
|
|
168
|
-
if (!values.length) {
|
|
169
|
-
return 0;
|
|
170
|
-
}
|
|
171
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
172
|
-
const middle = Math.floor(sorted.length / 2);
|
|
173
|
-
return sorted.length % 2
|
|
174
|
-
? sorted[middle]
|
|
175
|
-
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
176
|
-
}
|
|
177
|
-
function evaluateCalc(calc, values) {
|
|
178
|
-
const expression = calc.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
171
|
+
function evaluateCalc(calc, values, variables) {
|
|
172
|
+
const expression = calc
|
|
173
|
+
.replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
|
|
174
|
+
var _a;
|
|
175
|
+
const map = resolveVariablePath(variables, path);
|
|
176
|
+
const key = String((_a = values[keyField]) !== null && _a !== void 0 ? _a : '');
|
|
177
|
+
return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
|
|
178
|
+
? map[key]
|
|
179
|
+
: Number(defaultValue)));
|
|
180
|
+
})
|
|
181
|
+
.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
179
182
|
if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
|
|
180
183
|
throw new Error(`Unsupported calc expression: ${calc}`);
|
|
181
184
|
}
|
|
182
185
|
return Function(`"use strict"; return (${expression});`)();
|
|
183
186
|
}
|
|
187
|
+
function resolveVariablePath(variables, path) {
|
|
188
|
+
return path
|
|
189
|
+
.replace(VARIABLE_PATH_PREFIX_RE, '')
|
|
190
|
+
.split('.')
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
|
|
193
|
+
}
|
|
184
194
|
function sortRows(rows, orderBy = []) {
|
|
185
195
|
if (!orderBy.length) {
|
|
186
196
|
return rows;
|
|
@@ -213,7 +223,7 @@ function getBackendSort(orderBy) {
|
|
|
213
223
|
function getColumns(rows, query) {
|
|
214
224
|
var _a, _b, _c, _d;
|
|
215
225
|
const selectColumns = [
|
|
216
|
-
...((_a = query.
|
|
226
|
+
...((_a = query.group_by) !== null && _a !== void 0 ? _a : []).map(getGroupByAlias),
|
|
217
227
|
...((_b = query.select) !== null && _b !== void 0 ? _b : []).map(getSelectAlias),
|
|
218
228
|
...((_c = query.calcs) !== null && _c !== void 0 ? _c : []).map((item) => item.as),
|
|
219
229
|
].filter(Boolean);
|
|
@@ -225,7 +235,7 @@ function getDefaultSelect(rows) {
|
|
|
225
235
|
}
|
|
226
236
|
function isAggregateQuery(query) {
|
|
227
237
|
var _a, _b;
|
|
228
|
-
return Boolean(((_a = query.
|
|
238
|
+
return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
|
|
229
239
|
|| ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
|
|
230
240
|
}
|
|
231
241
|
function isFieldSelectItem(item) {
|
|
@@ -244,15 +254,133 @@ function getSelectAlias(item) {
|
|
|
244
254
|
}
|
|
245
255
|
return item.as;
|
|
246
256
|
}
|
|
247
|
-
function getGroupByField(item) {
|
|
248
|
-
return typeof item === 'string' ? item : item.field;
|
|
249
|
-
}
|
|
250
257
|
function getGroupByAlias(item) {
|
|
251
258
|
var _a;
|
|
252
259
|
return typeof item === 'string' ? item : (_a = item.as) !== null && _a !== void 0 ? _a : item.field;
|
|
253
260
|
}
|
|
254
|
-
function
|
|
255
|
-
|
|
261
|
+
function getEffectiveGroupBy(groupBy, select) {
|
|
262
|
+
if (groupBy === null || groupBy === void 0 ? void 0 : groupBy.length) {
|
|
263
|
+
return groupBy.map((item) => normalizeGroupByItem(item));
|
|
264
|
+
}
|
|
265
|
+
return select
|
|
266
|
+
.filter(isFieldSelectItem)
|
|
267
|
+
.map((item) => normalizeGroupByItem({ field: item.field, as: item.as, grain: item.grain }));
|
|
268
|
+
}
|
|
269
|
+
function normalizeGroupByItem(item) {
|
|
270
|
+
var _a;
|
|
271
|
+
if (typeof item === 'string') {
|
|
272
|
+
return { field: item, as: item };
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
field: item.field,
|
|
276
|
+
as: (_a = item.as) !== null && _a !== void 0 ? _a : item.field,
|
|
277
|
+
grain: item.grain,
|
|
278
|
+
timezone: 'timezone' in item ? item.timezone : undefined,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function toAggregateGroupByRule(item) {
|
|
282
|
+
if (item.grain) {
|
|
283
|
+
return {
|
|
284
|
+
type: 'date_trunc',
|
|
285
|
+
field: item.field,
|
|
286
|
+
truncation: item.grain,
|
|
287
|
+
timezone: item.timezone,
|
|
288
|
+
as: item.as,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
type: 'field',
|
|
293
|
+
field: item.field,
|
|
294
|
+
as: item.as,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function toAggregationRule(item) {
|
|
298
|
+
switch (item.agg) {
|
|
299
|
+
case 'count':
|
|
300
|
+
return { operation: 'count' };
|
|
301
|
+
case 'count_distinct':
|
|
302
|
+
return { operation: 'count_distinct', field: item.field };
|
|
303
|
+
case 'sum':
|
|
304
|
+
return { operation: 'sum', field: item.field };
|
|
305
|
+
case 'avg':
|
|
306
|
+
return { operation: 'avg', field: item.field };
|
|
307
|
+
case 'min':
|
|
308
|
+
return { operation: 'min', field: item.field };
|
|
309
|
+
case 'max':
|
|
310
|
+
return { operation: 'max', field: item.field };
|
|
311
|
+
case 'median':
|
|
312
|
+
return { operation: 'median', field: item.field };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function extractAggregateGroupValues(row, groupBy) {
|
|
316
|
+
return Object.fromEntries(groupBy.map((item) => [
|
|
317
|
+
item.as,
|
|
318
|
+
formatGroupValue(row[item.as], item.grain),
|
|
319
|
+
]));
|
|
320
|
+
}
|
|
321
|
+
function ensureAggregateGroup(groups, row, groupBy) {
|
|
322
|
+
const groupValues = groupBy.length ? extractAggregateGroupValues(row, groupBy) : {};
|
|
323
|
+
const key = JSON.stringify(groupValues);
|
|
324
|
+
const existingGroup = groups.get(key);
|
|
325
|
+
if (existingGroup) {
|
|
326
|
+
return existingGroup;
|
|
327
|
+
}
|
|
328
|
+
groups.set(key, groupValues);
|
|
329
|
+
return groupValues;
|
|
330
|
+
}
|
|
331
|
+
function applyAggregateDefaults(values, select) {
|
|
332
|
+
for (const item of select) {
|
|
333
|
+
if (typeof values[item.as] === 'undefined') {
|
|
334
|
+
values[item.as] = 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return values;
|
|
338
|
+
}
|
|
339
|
+
function groupAggregateSelectItems(select) {
|
|
340
|
+
var _a;
|
|
341
|
+
const groups = new Map();
|
|
342
|
+
for (const item of select) {
|
|
343
|
+
const filters = getAdminForthFilters(item.filters);
|
|
344
|
+
const key = getFilterCacheKey(filters);
|
|
345
|
+
const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { filters, items: [] };
|
|
346
|
+
group.items.push(item);
|
|
347
|
+
groups.set(key, group);
|
|
348
|
+
}
|
|
349
|
+
return Array.from(groups.values());
|
|
350
|
+
}
|
|
351
|
+
function getFilterCacheKey(filters) {
|
|
352
|
+
if (Array.isArray(filters) && !filters.length) {
|
|
353
|
+
return '__base__';
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify(filters);
|
|
356
|
+
}
|
|
357
|
+
function mergeFilters(...filters) {
|
|
358
|
+
const merged = [];
|
|
359
|
+
for (const filter of filters) {
|
|
360
|
+
const normalized = getAdminForthFilters(filter);
|
|
361
|
+
if (Array.isArray(normalized)) {
|
|
362
|
+
merged.push(...normalized);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (normalized) {
|
|
366
|
+
merged.push(normalized);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!merged.length) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
373
|
+
}
|
|
374
|
+
function getHiddenAggregateAlias(groupBy, select) {
|
|
375
|
+
const usedAliases = new Set([
|
|
376
|
+
...groupBy.map((item) => item.as),
|
|
377
|
+
...select.map((item) => item.as),
|
|
378
|
+
]);
|
|
379
|
+
let alias = '__adminforth_dashboard_group_seed__';
|
|
380
|
+
while (usedAliases.has(alias)) {
|
|
381
|
+
alias = `_${alias}`;
|
|
382
|
+
}
|
|
383
|
+
return alias;
|
|
256
384
|
}
|
|
257
385
|
function formatGroupValue(value, grain) {
|
|
258
386
|
if (!grain) {
|
|
@@ -265,9 +393,6 @@ function formatGroupValue(value, grain) {
|
|
|
265
393
|
if (grain === 'year') {
|
|
266
394
|
return `${date.getUTCFullYear()}`;
|
|
267
395
|
}
|
|
268
|
-
if (grain === 'quarter') {
|
|
269
|
-
return `${date.getUTCFullYear()}-Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
|
|
270
|
-
}
|
|
271
396
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
272
397
|
if (grain === 'month') {
|
|
273
398
|
return `${date.getUTCFullYear()}-${month}`;
|
|
@@ -281,123 +406,58 @@ function formatGroupValue(value, grain) {
|
|
|
281
406
|
if (grain === 'day') {
|
|
282
407
|
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
283
408
|
}
|
|
284
|
-
|
|
285
|
-
return `${date.getUTCFullYear()}-${month}-${day}T${hour}:00:00.000Z`;
|
|
409
|
+
return value;
|
|
286
410
|
}
|
|
287
|
-
function
|
|
411
|
+
function getAdminForthFilters(filters) {
|
|
288
412
|
if (Array.isArray(filters)) {
|
|
289
|
-
return filters.map((filter) =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return normalizeFilterNode(filters);
|
|
293
|
-
}
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
function normalizeFilterNode(filter) {
|
|
297
|
-
if (!isRecord(filter)) {
|
|
298
|
-
return filter;
|
|
299
|
-
}
|
|
300
|
-
if (Array.isArray(filter.and)) {
|
|
301
|
-
return Filters.AND(filter.and.map((item) => normalizeFilterNode(item)));
|
|
413
|
+
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
414
|
+
? toAdminForthFilter(filter)
|
|
415
|
+
: filter);
|
|
302
416
|
}
|
|
303
|
-
if (
|
|
304
|
-
return
|
|
417
|
+
if (isDashboardFilterExpression(filters)) {
|
|
418
|
+
return toAdminForthFilter(filters);
|
|
305
419
|
}
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
|
|
309
|
-
}
|
|
310
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
311
|
-
return Filters.NEQ(filter.field, normalizeFilterValue(filter.neq));
|
|
312
|
-
}
|
|
313
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
314
|
-
return Filters.GT(filter.field, normalizeFilterValue(filter.gt));
|
|
315
|
-
}
|
|
316
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
317
|
-
return Filters.GTE(filter.field, normalizeFilterValue(filter.gte));
|
|
318
|
-
}
|
|
319
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
320
|
-
return Filters.LT(filter.field, normalizeFilterValue(filter.lt));
|
|
321
|
-
}
|
|
322
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
|
|
323
|
-
return Filters.LTE(filter.field, normalizeFilterValue(filter.lte));
|
|
324
|
-
}
|
|
325
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
|
|
326
|
-
return Filters.IN(filter.field, normalizeFilterValue(filter.in));
|
|
327
|
-
}
|
|
328
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
329
|
-
return Filters.NOT_IN(filter.field, normalizeFilterValue(filter.not_in));
|
|
330
|
-
}
|
|
420
|
+
if (filters) {
|
|
421
|
+
return filters;
|
|
331
422
|
}
|
|
332
|
-
return
|
|
423
|
+
return [];
|
|
333
424
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
425
|
+
function isDashboardFilterExpression(value) {
|
|
426
|
+
if (Array.isArray(value)) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (!isRecord(value)) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return 'and' in value
|
|
433
|
+
|| 'or' in value
|
|
434
|
+
|| 'eq' in value
|
|
435
|
+
|| 'neq' in value
|
|
436
|
+
|| 'gt' in value
|
|
437
|
+
|| 'gte' in value
|
|
438
|
+
|| 'lt' in value
|
|
439
|
+
|| 'lte' in value
|
|
440
|
+
|| 'in' in value
|
|
441
|
+
|| 'not_in' in value
|
|
442
|
+
|| 'like' in value
|
|
443
|
+
|| 'ilike' in value;
|
|
444
|
+
}
|
|
445
|
+
function toAdminForthFilter(filter) {
|
|
336
446
|
if (Array.isArray(filter)) {
|
|
337
|
-
return filter.
|
|
447
|
+
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
338
448
|
}
|
|
339
449
|
if ('and' in filter) {
|
|
340
|
-
return filter.and.
|
|
450
|
+
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
341
451
|
}
|
|
342
452
|
if ('or' in filter) {
|
|
343
|
-
return filter.or.
|
|
344
|
-
}
|
|
345
|
-
const value = row[filter.field];
|
|
346
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
|
|
347
|
-
return value === normalizeFilterValue(filter.eq);
|
|
348
|
-
}
|
|
349
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
350
|
-
return value !== normalizeFilterValue(filter.neq);
|
|
351
|
-
}
|
|
352
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
353
|
-
return compareComparableValues(value, normalizeFilterValue(filter.gt)) > 0;
|
|
354
|
-
}
|
|
355
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
356
|
-
return compareComparableValues(value, normalizeFilterValue(filter.gte)) >= 0;
|
|
357
|
-
}
|
|
358
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
359
|
-
return compareComparableValues(value, normalizeFilterValue(filter.lt)) < 0;
|
|
453
|
+
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
360
454
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return (_b = (_a = filter.in) === null || _a === void 0 ? void 0 : _a.includes(value)) !== null && _b !== void 0 ? _b : false;
|
|
366
|
-
}
|
|
367
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
368
|
-
return !((_d = (_c = filter.not_in) === null || _c === void 0 ? void 0 : _c.includes(value)) !== null && _d !== void 0 ? _d : false);
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
function compareComparableValues(left, right) {
|
|
373
|
-
const leftNumber = Number(left);
|
|
374
|
-
const rightNumber = Number(right);
|
|
375
|
-
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
376
|
-
return leftNumber - rightNumber;
|
|
377
|
-
}
|
|
378
|
-
return String(left !== null && left !== void 0 ? left : '').localeCompare(String(right !== null && right !== void 0 ? right : ''));
|
|
379
|
-
}
|
|
380
|
-
function normalizeFilterValue(value) {
|
|
381
|
-
if (!isRecord(value) || typeof value.now_minus !== 'string') {
|
|
382
|
-
return value;
|
|
383
|
-
}
|
|
384
|
-
const match = value.now_minus.match(NOW_MINUS_RE);
|
|
385
|
-
if (!match) {
|
|
386
|
-
return value;
|
|
387
|
-
}
|
|
388
|
-
const amount = Number(match[1]);
|
|
389
|
-
const unit = match[2];
|
|
390
|
-
const date = new Date();
|
|
391
|
-
if (unit === 'h') {
|
|
392
|
-
date.setHours(date.getHours() - amount);
|
|
393
|
-
}
|
|
394
|
-
else if (unit === 'w') {
|
|
395
|
-
date.setDate(date.getDate() - amount * 7);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
date.setDate(date.getDate() - amount);
|
|
455
|
+
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
456
|
+
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
457
|
+
return createFilter(filter.field, filter[operator]);
|
|
458
|
+
}
|
|
399
459
|
}
|
|
400
|
-
return
|
|
460
|
+
return Filters.AND([]);
|
|
401
461
|
}
|
|
402
462
|
function toFiniteNumber(value) {
|
|
403
463
|
const numberValue = typeof value === 'number' ? value : Number(value);
|
package/endpoint/dashboard.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import type { IHttpServer } from 'adminforth';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
1
|
+
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
+
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
import {
|
|
4
|
+
DashboardApiResponseSchema,
|
|
5
|
+
SetDashboardConfigRequestSchema,
|
|
6
|
+
SlugRequestSchema,
|
|
7
|
+
} from '../schema/api.js';
|
|
8
|
+
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
5
9
|
|
|
6
10
|
type DashboardEndpointsContext = {
|
|
11
|
+
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
7
12
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
13
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
14
|
+
persistDashboardConfig: (
|
|
15
|
+
dashboard: DashboardRecord,
|
|
16
|
+
config: DashboardConfig,
|
|
17
|
+
) => Promise<PersistedDashboardResponse>;
|
|
8
18
|
};
|
|
9
19
|
|
|
10
20
|
export function registerDashboardEndpoints(
|
|
@@ -18,15 +28,45 @@ export function registerDashboardEndpoints(
|
|
|
18
28
|
request_schema: SlugRequestSchema,
|
|
19
29
|
response_schema: DashboardApiResponseSchema,
|
|
20
30
|
handler: async ({ body, response }) => {
|
|
21
|
-
const
|
|
22
|
-
const dashboard = await ctx.getDashboardRecord(slug);
|
|
31
|
+
const dashboard = await ctx.getDashboardRecord(body.slug);
|
|
23
32
|
|
|
24
33
|
if (!dashboard) {
|
|
25
34
|
response.setStatus(404);
|
|
26
35
|
return { error: 'Dashboard not found' };
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
return
|
|
38
|
+
return {
|
|
39
|
+
id: dashboard.id,
|
|
40
|
+
slug: dashboard.slug,
|
|
41
|
+
label: dashboard.label,
|
|
42
|
+
revision: dashboard.revision,
|
|
43
|
+
config: ctx.parseStoredDashboardConfig(dashboard.config),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
server.endpoint({
|
|
49
|
+
method: 'POST',
|
|
50
|
+
path: '/dashboard/set_dashboard_config',
|
|
51
|
+
description: 'Replaces one dashboard configuration, including groups and widgets. Superadmin only.',
|
|
52
|
+
request_schema: SetDashboardConfigRequestSchema,
|
|
53
|
+
response_schema: DashboardApiResponseSchema,
|
|
54
|
+
handler: async ({ body, adminUser, response }) => {
|
|
55
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
56
|
+
response.setStatus(403);
|
|
57
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const dashboard = await ctx.getDashboardRecord(body.slug);
|
|
61
|
+
|
|
62
|
+
if (!dashboard) {
|
|
63
|
+
response.setStatus(404);
|
|
64
|
+
return { error: 'Dashboard not found' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const config = body.config as DashboardConfig;
|
|
68
|
+
|
|
69
|
+
return ctx.persistDashboardConfig(dashboard, config);
|
|
30
70
|
},
|
|
31
71
|
});
|
|
32
72
|
}
|