@adminforth/dashboard 1.4.0 → 1.4.2
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 +23 -4
- package/custom/api/dashboardApi.ts +6 -9
- package/custom/model/dashboard.types.ts +60 -275
- package/custom/model/dashboardTopics.ts +5 -0
- package/custom/package.json +1 -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 +2 -2
- 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 +4 -15
- 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 -8
- package/dist/custom/api/dashboardApi.js +2 -6
- package/dist/custom/api/dashboardApi.ts +6 -9
- package/dist/custom/composables/useElementSize.js +7 -10
- package/dist/custom/model/dashboard.types.d.ts +38 -32
- package/dist/custom/model/dashboard.types.js +4 -161
- package/dist/custom/model/dashboard.types.ts +60 -275
- package/dist/custom/model/dashboardTopics.d.ts +2 -0
- package/dist/custom/model/dashboardTopics.js +4 -0
- package/dist/custom/model/dashboardTopics.ts +5 -0
- package/dist/custom/package.json +1 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +96 -96
- package/dist/custom/queries/useDashboardConfig.js +9 -12
- package/dist/custom/queries/useWidgetData.d.ts +96 -96
- package/dist/custom/queries/useWidgetData.js +9 -12
- 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 +2 -2
- 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 +4 -15
- 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 +1 -25
- package/dist/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/widgets/chart/chart.utils.js +6 -14
- package/dist/custom/widgets/registry.js +14 -22
- package/dist/endpoint/dashboard.d.ts +2 -3
- package/dist/endpoint/dashboard.js +12 -32
- package/dist/endpoint/groups.d.ts +2 -21
- package/dist/endpoint/groups.js +18 -16
- package/dist/endpoint/widgets.d.ts +0 -3
- package/dist/endpoint/widgets.js +27 -74
- package/dist/index.js +1 -3
- package/dist/schema/api.d.ts +2090 -511
- package/dist/schema/api.js +18 -15
- package/dist/schema/widget.d.ts +1003 -250
- package/dist/schema/widget.js +102 -46
- package/dist/services/dashboardConfigService.d.ts +0 -10
- package/dist/services/dashboardConfigService.js +6 -21
- package/dist/services/widgetDataService.js +226 -196
- package/endpoint/dashboard.ts +13 -46
- package/endpoint/groups.ts +25 -42
- package/endpoint/widgets.ts +36 -95
- package/index.ts +0 -3
- package/package.json +3 -3
- package/schema/api.ts +19 -15
- package/schema/widget.ts +113 -52
- package/services/dashboardConfigService.ts +6 -25
- package/services/widgetDataService.ts +304 -229
- 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,11 +8,22 @@ 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;
|
|
13
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;
|
|
14
13
|
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
15
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
|
+
};
|
|
16
27
|
export function getWidgetData(adminforth_1, widget_1) {
|
|
17
28
|
return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
|
|
18
29
|
var _a, _b;
|
|
@@ -40,15 +51,15 @@ function getFunnelWidgetData(adminforth, query, variables) {
|
|
|
40
51
|
return __awaiter(this, void 0, void 0, function* () {
|
|
41
52
|
var _a;
|
|
42
53
|
const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
-
var _a;
|
|
54
|
+
var _a, _b;
|
|
44
55
|
const valueField = step.metric.as;
|
|
45
|
-
const
|
|
56
|
+
const [values = {}] = yield getAggregateRows(adminforth, step.resource, step.filters, [step.metric], []);
|
|
46
57
|
const row = {
|
|
47
58
|
name: step.name,
|
|
48
59
|
resource: step.resource,
|
|
49
|
-
[valueField]:
|
|
60
|
+
[valueField]: (_a = values[valueField]) !== null && _a !== void 0 ? _a : 0,
|
|
50
61
|
};
|
|
51
|
-
for (const calc of (
|
|
62
|
+
for (const calc of (_b = query.calcs) !== null && _b !== void 0 ? _b : []) {
|
|
52
63
|
row[calc.as] = evaluateCalc(calc.calc, row, variables);
|
|
53
64
|
}
|
|
54
65
|
return row;
|
|
@@ -67,9 +78,10 @@ function getFunnelWidgetData(adminforth, query, variables) {
|
|
|
67
78
|
function getQueryWidgetData(adminforth, query, variables) {
|
|
68
79
|
return __awaiter(this, void 0, void 0, function* () {
|
|
69
80
|
var _a, _b, _c;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
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);
|
|
73
85
|
const slicedRows = typeof query.limit === 'number'
|
|
74
86
|
? orderedRows.slice((_a = query.offset) !== null && _a !== void 0 ? _a : 0, ((_b = query.offset) !== null && _b !== void 0 ? _b : 0) + query.limit)
|
|
75
87
|
: orderedRows.slice((_c = query.offset) !== null && _c !== void 0 ? _c : 0);
|
|
@@ -87,52 +99,55 @@ function getQueryWidgetData(adminforth, query, variables) {
|
|
|
87
99
|
}
|
|
88
100
|
function getResourceRows(adminforth, resourceId, filters, sort) {
|
|
89
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
-
return adminforth.resource(resourceId).list(
|
|
102
|
+
return adminforth.resource(resourceId).list(getAdminForthFilters(filters), undefined, 0, sort);
|
|
91
103
|
});
|
|
92
104
|
}
|
|
93
|
-
function
|
|
94
|
-
var _a
|
|
105
|
+
function buildPlainQueryRows(rows, query, variables) {
|
|
106
|
+
var _a;
|
|
95
107
|
const select = (_a = query.select) !== null && _a !== void 0 ? _a : getDefaultSelect(rows);
|
|
96
|
-
const groupBy = (_b = query.groupBy) !== null && _b !== void 0 ? _b : [];
|
|
97
|
-
if (isAggregateQuery(query)) {
|
|
98
|
-
return buildGroupedRows(rows, select, groupBy, variables, query.calcs);
|
|
99
|
-
}
|
|
100
108
|
return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
|
|
101
109
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
for (const row of rows) {
|
|
113
|
-
const values = Object.fromEntries(effectiveGroupBy.map((item) => {
|
|
114
|
-
const field = getGroupByField(item);
|
|
115
|
-
const alias = getGroupByAlias(item);
|
|
116
|
-
const grain = getGroupByGrain(item);
|
|
117
|
-
return [alias, formatGroupValue(row[field], grain)];
|
|
118
|
-
}));
|
|
119
|
-
const key = JSON.stringify(values);
|
|
120
|
-
const group = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { values, rows: [] };
|
|
121
|
-
group.rows.push(row);
|
|
122
|
-
groups.set(key, group);
|
|
123
|
-
}
|
|
124
|
-
return Array.from(groups.values()).map((group) => (Object.assign(Object.assign({}, group.values), calculateGroupValues(group.rows, select, calcs, variables, group.values))));
|
|
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
|
+
});
|
|
125
119
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
133
|
}
|
|
135
|
-
|
|
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);
|
|
136
151
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
137
152
|
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
138
153
|
}
|
|
@@ -153,41 +168,6 @@ function buildPlainRow(row, select, calcs = [], variables) {
|
|
|
153
168
|
}
|
|
154
169
|
return values;
|
|
155
170
|
}
|
|
156
|
-
function calculateAggregate(rows, item) {
|
|
157
|
-
switch (item.agg) {
|
|
158
|
-
case 'count':
|
|
159
|
-
return rows.length;
|
|
160
|
-
case 'count_distinct':
|
|
161
|
-
return new Set(rows.map((row) => row[item.field])).size;
|
|
162
|
-
case 'sum':
|
|
163
|
-
return aggregateNumbers(rows, item.field, (values) => values.reduce((sum, value) => sum + value, 0));
|
|
164
|
-
case 'avg':
|
|
165
|
-
return aggregateNumbers(rows, item.field, (values) => values.length
|
|
166
|
-
? values.reduce((sum, value) => sum + value, 0) / values.length
|
|
167
|
-
: 0);
|
|
168
|
-
case 'min':
|
|
169
|
-
return aggregateNumbers(rows, item.field, (values) => values.length ? Math.min(...values) : 0);
|
|
170
|
-
case 'max':
|
|
171
|
-
return aggregateNumbers(rows, item.field, (values) => values.length ? Math.max(...values) : 0);
|
|
172
|
-
case 'median':
|
|
173
|
-
return aggregateNumbers(rows, item.field, calculateMedian);
|
|
174
|
-
default:
|
|
175
|
-
throw new Error(`Unsupported aggregation operation: ${item.agg}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function aggregateNumbers(rows, field, aggregate) {
|
|
179
|
-
return aggregate(rows.map((row) => toFiniteNumber(row[field])).filter(Number.isFinite));
|
|
180
|
-
}
|
|
181
|
-
function calculateMedian(values) {
|
|
182
|
-
if (!values.length) {
|
|
183
|
-
return 0;
|
|
184
|
-
}
|
|
185
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
186
|
-
const middle = Math.floor(sorted.length / 2);
|
|
187
|
-
return sorted.length % 2
|
|
188
|
-
? sorted[middle]
|
|
189
|
-
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
190
|
-
}
|
|
191
171
|
function evaluateCalc(calc, values, variables) {
|
|
192
172
|
const expression = calc
|
|
193
173
|
.replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
|
|
@@ -243,7 +223,7 @@ function getBackendSort(orderBy) {
|
|
|
243
223
|
function getColumns(rows, query) {
|
|
244
224
|
var _a, _b, _c, _d;
|
|
245
225
|
const selectColumns = [
|
|
246
|
-
...((_a = query.
|
|
226
|
+
...((_a = query.group_by) !== null && _a !== void 0 ? _a : []).map(getGroupByAlias),
|
|
247
227
|
...((_b = query.select) !== null && _b !== void 0 ? _b : []).map(getSelectAlias),
|
|
248
228
|
...((_c = query.calcs) !== null && _c !== void 0 ? _c : []).map((item) => item.as),
|
|
249
229
|
].filter(Boolean);
|
|
@@ -255,7 +235,7 @@ function getDefaultSelect(rows) {
|
|
|
255
235
|
}
|
|
256
236
|
function isAggregateQuery(query) {
|
|
257
237
|
var _a, _b;
|
|
258
|
-
return Boolean(((_a = query.
|
|
238
|
+
return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
|
|
259
239
|
|| ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
|
|
260
240
|
}
|
|
261
241
|
function isFieldSelectItem(item) {
|
|
@@ -274,15 +254,133 @@ function getSelectAlias(item) {
|
|
|
274
254
|
}
|
|
275
255
|
return item.as;
|
|
276
256
|
}
|
|
277
|
-
function getGroupByField(item) {
|
|
278
|
-
return typeof item === 'string' ? item : item.field;
|
|
279
|
-
}
|
|
280
257
|
function getGroupByAlias(item) {
|
|
281
258
|
var _a;
|
|
282
259
|
return typeof item === 'string' ? item : (_a = item.as) !== null && _a !== void 0 ? _a : item.field;
|
|
283
260
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
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;
|
|
286
384
|
}
|
|
287
385
|
function formatGroupValue(value, grain) {
|
|
288
386
|
if (!grain) {
|
|
@@ -295,9 +393,6 @@ function formatGroupValue(value, grain) {
|
|
|
295
393
|
if (grain === 'year') {
|
|
296
394
|
return `${date.getUTCFullYear()}`;
|
|
297
395
|
}
|
|
298
|
-
if (grain === 'quarter') {
|
|
299
|
-
return `${date.getUTCFullYear()}-Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
|
|
300
|
-
}
|
|
301
396
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
302
397
|
if (grain === 'month') {
|
|
303
398
|
return `${date.getUTCFullYear()}-${month}`;
|
|
@@ -311,123 +406,58 @@ function formatGroupValue(value, grain) {
|
|
|
311
406
|
if (grain === 'day') {
|
|
312
407
|
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
313
408
|
}
|
|
314
|
-
|
|
315
|
-
return `${date.getUTCFullYear()}-${month}-${day}T${hour}:00:00.000Z`;
|
|
409
|
+
return value;
|
|
316
410
|
}
|
|
317
|
-
function
|
|
411
|
+
function getAdminForthFilters(filters) {
|
|
318
412
|
if (Array.isArray(filters)) {
|
|
319
|
-
return filters.map((filter) =>
|
|
413
|
+
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
414
|
+
? toAdminForthFilter(filter)
|
|
415
|
+
: filter);
|
|
320
416
|
}
|
|
321
|
-
if (
|
|
322
|
-
return
|
|
417
|
+
if (isDashboardFilterExpression(filters)) {
|
|
418
|
+
return toAdminForthFilter(filters);
|
|
323
419
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
function normalizeFilterNode(filter) {
|
|
327
|
-
if (!isRecord(filter)) {
|
|
328
|
-
return filter;
|
|
420
|
+
if (filters) {
|
|
421
|
+
return filters;
|
|
329
422
|
}
|
|
330
|
-
|
|
331
|
-
return Filters.AND(filter.and.map((item) => normalizeFilterNode(item)));
|
|
332
|
-
}
|
|
333
|
-
if (Array.isArray(filter.or)) {
|
|
334
|
-
return Filters.OR(filter.or.map((item) => normalizeFilterNode(item)));
|
|
335
|
-
}
|
|
336
|
-
if (typeof filter.field === 'string') {
|
|
337
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
|
|
338
|
-
return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
|
|
339
|
-
}
|
|
340
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
341
|
-
return Filters.NEQ(filter.field, normalizeFilterValue(filter.neq));
|
|
342
|
-
}
|
|
343
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
344
|
-
return Filters.GT(filter.field, normalizeFilterValue(filter.gt));
|
|
345
|
-
}
|
|
346
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
347
|
-
return Filters.GTE(filter.field, normalizeFilterValue(filter.gte));
|
|
348
|
-
}
|
|
349
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
350
|
-
return Filters.LT(filter.field, normalizeFilterValue(filter.lt));
|
|
351
|
-
}
|
|
352
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
|
|
353
|
-
return Filters.LTE(filter.field, normalizeFilterValue(filter.lte));
|
|
354
|
-
}
|
|
355
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
|
|
356
|
-
return Filters.IN(filter.field, normalizeFilterValue(filter.in));
|
|
357
|
-
}
|
|
358
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
359
|
-
return Filters.NOT_IN(filter.field, normalizeFilterValue(filter.not_in));
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return filter;
|
|
423
|
+
return [];
|
|
363
424
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
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) {
|
|
366
446
|
if (Array.isArray(filter)) {
|
|
367
|
-
return filter.
|
|
447
|
+
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
368
448
|
}
|
|
369
449
|
if ('and' in filter) {
|
|
370
|
-
return filter.and.
|
|
450
|
+
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
371
451
|
}
|
|
372
452
|
if ('or' in filter) {
|
|
373
|
-
return filter.or.
|
|
374
|
-
}
|
|
375
|
-
const value = row[filter.field];
|
|
376
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
|
|
377
|
-
return value === normalizeFilterValue(filter.eq);
|
|
378
|
-
}
|
|
379
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
380
|
-
return value !== normalizeFilterValue(filter.neq);
|
|
381
|
-
}
|
|
382
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
383
|
-
return compareComparableValues(value, normalizeFilterValue(filter.gt)) > 0;
|
|
384
|
-
}
|
|
385
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
386
|
-
return compareComparableValues(value, normalizeFilterValue(filter.gte)) >= 0;
|
|
387
|
-
}
|
|
388
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
389
|
-
return compareComparableValues(value, normalizeFilterValue(filter.lt)) < 0;
|
|
453
|
+
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
390
454
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return (_b = (_a = filter.in) === null || _a === void 0 ? void 0 : _a.includes(value)) !== null && _b !== void 0 ? _b : false;
|
|
396
|
-
}
|
|
397
|
-
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
398
|
-
return !((_d = (_c = filter.not_in) === null || _c === void 0 ? void 0 : _c.includes(value)) !== null && _d !== void 0 ? _d : false);
|
|
399
|
-
}
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
function compareComparableValues(left, right) {
|
|
403
|
-
const leftNumber = Number(left);
|
|
404
|
-
const rightNumber = Number(right);
|
|
405
|
-
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
406
|
-
return leftNumber - rightNumber;
|
|
407
|
-
}
|
|
408
|
-
return String(left !== null && left !== void 0 ? left : '').localeCompare(String(right !== null && right !== void 0 ? right : ''));
|
|
409
|
-
}
|
|
410
|
-
function normalizeFilterValue(value) {
|
|
411
|
-
if (!isRecord(value) || typeof value.now_minus !== 'string') {
|
|
412
|
-
return value;
|
|
413
|
-
}
|
|
414
|
-
const match = value.now_minus.match(NOW_MINUS_RE);
|
|
415
|
-
if (!match) {
|
|
416
|
-
return value;
|
|
417
|
-
}
|
|
418
|
-
const amount = Number(match[1]);
|
|
419
|
-
const unit = match[2];
|
|
420
|
-
const date = new Date();
|
|
421
|
-
if (unit === 'h') {
|
|
422
|
-
date.setHours(date.getHours() - amount);
|
|
423
|
-
}
|
|
424
|
-
else if (unit === 'w') {
|
|
425
|
-
date.setDate(date.getDate() - amount * 7);
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
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
|
+
}
|
|
429
459
|
}
|
|
430
|
-
return
|
|
460
|
+
return Filters.AND([]);
|
|
431
461
|
}
|
|
432
462
|
function toFiniteNumber(value) {
|
|
433
463
|
const numberValue = typeof value === 'number' ? value : Number(value);
|
package/endpoint/dashboard.ts
CHANGED
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
-
import {
|
|
3
|
-
import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
2
|
+
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
4
3
|
import {
|
|
5
4
|
DashboardApiResponseSchema,
|
|
6
|
-
DashboardConfigZodSchema,
|
|
7
5
|
SetDashboardConfigRequestSchema,
|
|
8
6
|
SlugRequestSchema,
|
|
9
7
|
} from '../schema/api.js';
|
|
10
|
-
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
11
8
|
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
12
|
-
import { buildDashboardResponse } from '../services/dashboardConfigService.js';
|
|
13
9
|
|
|
14
10
|
type DashboardEndpointsContext = {
|
|
15
11
|
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
16
12
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
13
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
17
14
|
persistDashboardConfig: (
|
|
18
15
|
dashboard: DashboardRecord,
|
|
19
16
|
config: DashboardConfig,
|
|
20
17
|
) => Promise<PersistedDashboardResponse>;
|
|
21
|
-
validateDashboardWidgetApiConfig: (
|
|
22
|
-
widget: DashboardWidgetConfig,
|
|
23
|
-
) => DashboardWidgetConfigValidationError[];
|
|
24
18
|
};
|
|
25
19
|
|
|
26
|
-
function formatDashboardConfigValidationErrors(error: { issues: { path: PropertyKey[], message: string }[] }) {
|
|
27
|
-
return error.issues.map((issue) => ({
|
|
28
|
-
field: issue.path.length ? issue.path.map(String).join('.') : 'config',
|
|
29
|
-
message: issue.message,
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
20
|
export function registerDashboardEndpoints(
|
|
34
21
|
server: IHttpServer,
|
|
35
22
|
ctx: DashboardEndpointsContext,
|
|
@@ -41,15 +28,20 @@ export function registerDashboardEndpoints(
|
|
|
41
28
|
request_schema: SlugRequestSchema,
|
|
42
29
|
response_schema: DashboardApiResponseSchema,
|
|
43
30
|
handler: async ({ body, response }) => {
|
|
44
|
-
const
|
|
45
|
-
const dashboard = await ctx.getDashboardRecord(slug);
|
|
31
|
+
const dashboard = await ctx.getDashboardRecord(body.slug);
|
|
46
32
|
|
|
47
33
|
if (!dashboard) {
|
|
48
34
|
response.setStatus(404);
|
|
49
35
|
return { error: 'Dashboard not found' };
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
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
|
+
};
|
|
53
45
|
},
|
|
54
46
|
});
|
|
55
47
|
|
|
@@ -65,41 +57,16 @@ export function registerDashboardEndpoints(
|
|
|
65
57
|
return { error: 'Dashboard edit is not allowed' };
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
const
|
|
69
|
-
const dashboard = await ctx.getDashboardRecord(slug);
|
|
60
|
+
const dashboard = await ctx.getDashboardRecord(body.slug);
|
|
70
61
|
|
|
71
62
|
if (!dashboard) {
|
|
72
63
|
response.setStatus(404);
|
|
73
64
|
return { error: 'Dashboard not found' };
|
|
74
65
|
}
|
|
75
66
|
|
|
76
|
-
const
|
|
77
|
-
const parsedConfig = DashboardConfigZodSchema.safeParse(normalizedConfig);
|
|
78
|
-
|
|
79
|
-
if (!parsedConfig.success) {
|
|
80
|
-
response.setStatus(422);
|
|
81
|
-
return {
|
|
82
|
-
error: 'Invalid dashboard config',
|
|
83
|
-
validationErrors: formatDashboardConfigValidationErrors(parsedConfig.error),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const widgetValidationErrors = parsedConfig.data.widgets.flatMap((widget, index) => (
|
|
88
|
-
ctx.validateDashboardWidgetApiConfig(widget as DashboardWidgetConfig).map((error) => ({
|
|
89
|
-
...error,
|
|
90
|
-
field: `widgets.${index}.${error.field}`,
|
|
91
|
-
}))
|
|
92
|
-
));
|
|
93
|
-
|
|
94
|
-
if (widgetValidationErrors.length) {
|
|
95
|
-
response.setStatus(422);
|
|
96
|
-
return {
|
|
97
|
-
error: 'Invalid dashboard config',
|
|
98
|
-
validationErrors: widgetValidationErrors,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
67
|
+
const config = body.config as DashboardConfig;
|
|
101
68
|
|
|
102
|
-
return ctx.persistDashboardConfig(dashboard,
|
|
69
|
+
return ctx.persistDashboardConfig(dashboard, config);
|
|
103
70
|
},
|
|
104
71
|
});
|
|
105
72
|
}
|