@adminforth/dashboard 1.2.0 → 1.4.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.
Files changed (60) hide show
  1. package/README.md +116 -39
  2. package/custom/api/dashboardApi.ts +4 -0
  3. package/custom/composables/useElementSize.ts +17 -2
  4. package/custom/model/dashboard.types.ts +337 -236
  5. package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  6. package/custom/widgets/chart/ChartWidget.vue +38 -53
  7. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  8. package/custom/widgets/chart/chart.types.ts +17 -66
  9. package/custom/widgets/chart/chart.utils.ts +11 -0
  10. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  11. package/custom/widgets/chart/line/LineChart.vue +23 -15
  12. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  13. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  14. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  15. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  16. package/custom/widgets/table/TableWidget.vue +8 -3
  17. package/dist/custom/api/dashboardApi.d.ts +1 -0
  18. package/dist/custom/api/dashboardApi.js +5 -0
  19. package/dist/custom/api/dashboardApi.ts +4 -0
  20. package/dist/custom/composables/useElementSize.js +14 -2
  21. package/dist/custom/composables/useElementSize.ts +17 -2
  22. package/dist/custom/model/dashboard.types.d.ts +181 -61
  23. package/dist/custom/model/dashboard.types.js +82 -93
  24. package/dist/custom/model/dashboard.types.ts +337 -236
  25. package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
  26. package/dist/custom/queries/useWidgetData.d.ts +848 -62
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  28. package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
  31. package/dist/custom/widgets/chart/chart.types.js +2 -25
  32. package/dist/custom/widgets/chart/chart.types.ts +17 -66
  33. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  34. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  35. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  36. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  37. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  38. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  39. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  42. package/dist/custom/widgets/table/TableWidget.vue +8 -3
  43. package/dist/endpoint/dashboard.d.ts +7 -2
  44. package/dist/endpoint/dashboard.js +45 -1
  45. package/dist/endpoint/widgets.d.ts +2 -1
  46. package/dist/endpoint/widgets.js +6 -2
  47. package/dist/schema/api.d.ts +2773 -736
  48. package/dist/schema/api.js +5 -0
  49. package/dist/schema/widget.d.ts +1648 -476
  50. package/dist/schema/widget.js +208 -139
  51. package/dist/services/widgetConfigValidator.js +16 -40
  52. package/dist/services/widgetDataService.d.ts +2 -1
  53. package/dist/services/widgetDataService.js +389 -82
  54. package/endpoint/dashboard.ts +77 -4
  55. package/endpoint/widgets.ts +11 -4
  56. package/package.json +1 -1
  57. package/schema/api.ts +6 -0
  58. package/schema/widget.ts +225 -139
  59. package/services/widgetConfigValidator.ts +29 -53
  60. package/services/widgetDataService.ts +522 -100
@@ -7,124 +7,431 @@ 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 { Aggregates, GroupBy, Sorts } from 'adminforth';
10
+ import { Filters, Sorts } from 'adminforth';
11
+ const NOW_MINUS_RE = /^(\d+)([dhw])$/;
12
+ const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
13
+ 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
+ const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
15
+ const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
11
16
  export function getWidgetData(adminforth_1, widget_1) {
12
17
  return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
13
- const dataSource = getWidgetDataSource(widget.dataSource);
14
- if (!dataSource) {
18
+ var _a, _b;
19
+ if (!('query' in widget)) {
15
20
  return null;
16
21
  }
17
- if (dataSource.type === 'aggregate') {
18
- return getAggregateWidgetData(adminforth, dataSource);
22
+ const data = 'steps' in widget.query
23
+ ? yield getFunnelWidgetData(adminforth, widget.query, (_a = options.variables) !== null && _a !== void 0 ? _a : {})
24
+ : yield getQueryWidgetData(adminforth, widget.query, (_b = options.variables) !== null && _b !== void 0 ? _b : {});
25
+ if (widget.target !== 'table' || !options.pagination) {
26
+ return data;
19
27
  }
20
- return getResourceWidgetData(adminforth, dataSource, options);
28
+ const page = options.pagination.page;
29
+ const pageSize = options.pagination.pageSize;
30
+ const offset = (page - 1) * pageSize;
31
+ return Object.assign(Object.assign({}, data), { rows: data.rows.slice(offset, offset + pageSize), pagination: {
32
+ page,
33
+ pageSize,
34
+ total: data.rows.length,
35
+ totalPages: Math.max(Math.ceil(data.rows.length / pageSize), 1),
36
+ } });
21
37
  });
22
38
  }
23
- function getResourceWidgetData(adminforth, dataSource, options) {
39
+ function getFunnelWidgetData(adminforth, query, variables) {
24
40
  return __awaiter(this, void 0, void 0, function* () {
25
- var _a, _b;
26
- const resource = adminforth.resource(dataSource.resourceId);
27
- const filters = normalizeFilters(dataSource.filters);
28
- const sort = normalizeSort(dataSource.sort);
29
- const pagination = options.pagination;
30
- const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
31
- const limit = pagination ? pagination.pageSize : undefined;
32
- const rows = yield resource.list(filters, limit !== null && limit !== void 0 ? limit : undefined, offset, sort);
33
- const columns = (_a = dataSource.columns) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
34
- const total = pagination ? yield resource.count(filters) : 0;
35
- return Object.assign({ columns, rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))) }, (pagination ? {
36
- pagination: {
37
- page: pagination.page,
38
- pageSize: pagination.pageSize,
39
- total,
40
- totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
41
- },
42
- } : {}));
41
+ var _a;
42
+ const rows = yield Promise.all(query.steps.map((step) => __awaiter(this, void 0, void 0, function* () {
43
+ var _a;
44
+ const valueField = step.metric.as;
45
+ const sourceRows = yield getResourceRows(adminforth, step.resource, step.filters);
46
+ const row = {
47
+ name: step.name,
48
+ resource: step.resource,
49
+ [valueField]: calculateAggregate(sourceRows, step.metric),
50
+ };
51
+ for (const calc of (_a = query.calcs) !== null && _a !== void 0 ? _a : []) {
52
+ row[calc.as] = evaluateCalc(calc.calc, row, variables);
53
+ }
54
+ return row;
55
+ })));
56
+ return {
57
+ kind: 'aggregate',
58
+ columns: [
59
+ 'name',
60
+ ...Array.from(new Set(query.steps.map((step) => step.metric.as))),
61
+ ...Array.from(new Set(((_a = query.calcs) !== null && _a !== void 0 ? _a : []).map((calc) => calc.as))),
62
+ ],
63
+ rows,
64
+ };
43
65
  });
44
66
  }
45
- function getAggregateWidgetData(adminforth, dataSource) {
67
+ function getQueryWidgetData(adminforth, query, variables) {
46
68
  return __awaiter(this, void 0, void 0, function* () {
47
- var _a, _b;
48
- const resource = adminforth.resource(dataSource.resourceId);
49
- const rows = yield resource.aggregate(normalizeFilters(dataSource.filters), Object.fromEntries(Object.entries(dataSource.aggregations).map(([alias, rule]) => [
50
- alias,
51
- createAggregationRule(rule),
52
- ])), dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined);
53
- const columns = Object.keys((_a = rows[0]) !== null && _a !== void 0 ? _a : {});
54
- if (!dataSource.groupBy) {
55
- const values = (_b = rows[0]) !== null && _b !== void 0 ? _b : {};
56
- return {
57
- kind: 'aggregate',
58
- columns: Object.keys(values),
59
- rows: Object.keys(values).length ? [values] : [],
60
- values,
61
- };
69
+ var _a, _b, _c;
70
+ const rows = yield getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.orderBy));
71
+ const selectedRows = buildQueryRows(rows, query, variables);
72
+ const orderedRows = sortRows(selectedRows, query.orderBy);
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 = getColumns(slicedRows, query);
77
+ if (isAggregateQuery(query)) {
78
+ const values = slicedRows.length === 1 ? slicedRows[0] : undefined;
79
+ return Object.assign({ kind: 'aggregate', columns, rows: slicedRows }, (values ? { values } : {}));
62
80
  }
63
81
  return {
64
- kind: 'aggregate',
82
+ kind: 'table',
65
83
  columns,
66
- rows,
84
+ rows: slicedRows,
67
85
  };
68
86
  });
69
87
  }
70
- function getWidgetDataSource(dataSource) {
71
- if (isWidgetDataSource(dataSource)) {
72
- return dataSource;
73
- }
74
- return undefined;
88
+ function getResourceRows(adminforth, resourceId, filters, sort) {
89
+ return __awaiter(this, void 0, void 0, function* () {
90
+ return adminforth.resource(resourceId).list(normalizeFilters(filters), undefined, 0, sort);
91
+ });
75
92
  }
76
- function isWidgetDataSource(value) {
77
- return isRecord(value)
78
- && (value.type === 'resource' || value.type === 'aggregate')
79
- && typeof value.resourceId === 'string';
93
+ function buildQueryRows(rows, query, variables) {
94
+ var _a, _b;
95
+ 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
+ return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
80
101
  }
81
- function normalizeFilters(filters) {
82
- if (Array.isArray(filters)) {
83
- return filters;
102
+ function buildGroupedRows(rows, select, groupBy, variables, calcs = []) {
103
+ var _a;
104
+ const groups = new Map();
105
+ const effectiveGroupBy = groupBy.length
106
+ ? groupBy
107
+ : select.filter(isFieldSelectItem).map((item) => ({ field: item.field, as: item.as, grain: item.grain }));
108
+ if (!effectiveGroupBy.length) {
109
+ const values = calculateGroupValues(rows, select, calcs, variables);
110
+ return Object.keys(values).length ? [values] : [];
84
111
  }
85
- if (isRecord(filters)) {
86
- return filters;
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);
87
123
  }
88
- return [];
124
+ return Array.from(groups.values()).map((group) => (Object.assign(Object.assign({}, group.values), calculateGroupValues(group.rows, select, calcs, variables, group.values))));
89
125
  }
90
- function normalizeSort(sort) {
91
- if (Array.isArray(sort)) {
92
- return sort;
126
+ function calculateGroupValues(rows, select, calcs, variables, baseValues = {}) {
127
+ const values = Object.assign({}, baseValues);
128
+ for (const item of select) {
129
+ if (isAggregateSelectItem(item)) {
130
+ const filteredRows = item.filters
131
+ ? rows.filter((row) => matchesFilterExpression(row, item.filters))
132
+ : rows;
133
+ values[item.as] = calculateAggregate(filteredRows, item);
134
+ }
93
135
  }
94
- if (!isRecord(sort) || typeof sort.field !== 'string') {
95
- return undefined;
136
+ for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
137
+ values[item.as] = evaluateCalc(item.calc, values, variables);
96
138
  }
97
- if (sort.direction === 'asc') {
98
- return [Sorts.ASC(sort.field)];
139
+ return values;
140
+ }
141
+ function buildPlainRow(row, select, calcs = [], variables) {
142
+ var _a;
143
+ const values = {};
144
+ for (const item of select) {
145
+ if (isFieldSelectItem(item)) {
146
+ values[(_a = item.as) !== null && _a !== void 0 ? _a : item.field] = item.grain
147
+ ? formatGroupValue(row[item.field], item.grain)
148
+ : row[item.field];
149
+ }
99
150
  }
100
- if (sort.direction === 'desc') {
101
- return [Sorts.DESC(sort.field)];
151
+ for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
152
+ values[item.as] = evaluateCalc(item.calc, values, variables);
102
153
  }
103
- return sort;
154
+ return values;
104
155
  }
105
- function createAggregationRule(rule) {
106
- switch (rule.operation) {
107
- case 'sum':
108
- return Aggregates.sum(rule.field);
156
+ function calculateAggregate(rows, item) {
157
+ switch (item.agg) {
109
158
  case 'count':
110
- return Aggregates.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));
111
164
  case 'avg':
112
- return Aggregates.avg(rule.field);
165
+ return aggregateNumbers(rows, item.field, (values) => values.length
166
+ ? values.reduce((sum, value) => sum + value, 0) / values.length
167
+ : 0);
113
168
  case 'min':
114
- return Aggregates.min(rule.field);
169
+ return aggregateNumbers(rows, item.field, (values) => values.length ? Math.min(...values) : 0);
115
170
  case 'max':
116
- return Aggregates.max(rule.field);
171
+ return aggregateNumbers(rows, item.field, (values) => values.length ? Math.max(...values) : 0);
117
172
  case 'median':
118
- return Aggregates.median(rule.field);
173
+ return aggregateNumbers(rows, item.field, calculateMedian);
119
174
  default:
120
- throw new Error(`Unsupported aggregation operation: ${rule.operation}`);
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
+ function evaluateCalc(calc, values, variables) {
192
+ const expression = calc
193
+ .replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
194
+ var _a;
195
+ const map = resolveVariablePath(variables, path);
196
+ const key = String((_a = values[keyField]) !== null && _a !== void 0 ? _a : '');
197
+ return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
198
+ ? map[key]
199
+ : Number(defaultValue)));
200
+ })
201
+ .replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
202
+ if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
203
+ throw new Error(`Unsupported calc expression: ${calc}`);
204
+ }
205
+ return Function(`"use strict"; return (${expression});`)();
206
+ }
207
+ function resolveVariablePath(variables, path) {
208
+ return path
209
+ .replace(VARIABLE_PATH_PREFIX_RE, '')
210
+ .split('.')
211
+ .filter(Boolean)
212
+ .reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
213
+ }
214
+ function sortRows(rows, orderBy = []) {
215
+ if (!orderBy.length) {
216
+ return rows;
217
+ }
218
+ return [...rows].sort((left, right) => {
219
+ for (const order of orderBy) {
220
+ const direction = order.direction === 'asc' ? 1 : -1;
221
+ const result = compareValues(left[order.field], right[order.field]);
222
+ if (result !== 0) {
223
+ return result * direction;
224
+ }
225
+ }
226
+ return 0;
227
+ });
228
+ }
229
+ function compareValues(left, right) {
230
+ if (typeof left === 'number' && typeof right === 'number') {
231
+ return left - right;
232
+ }
233
+ return String(left !== null && left !== void 0 ? left : '').localeCompare(String(right !== null && right !== void 0 ? right : ''));
234
+ }
235
+ function getBackendSort(orderBy) {
236
+ if (!(orderBy === null || orderBy === void 0 ? void 0 : orderBy.length)) {
237
+ return undefined;
238
+ }
239
+ return orderBy.map((order) => order.direction === 'asc'
240
+ ? Sorts.ASC(order.field)
241
+ : Sorts.DESC(order.field));
242
+ }
243
+ function getColumns(rows, query) {
244
+ var _a, _b, _c, _d;
245
+ const selectColumns = [
246
+ ...((_a = query.groupBy) !== null && _a !== void 0 ? _a : []).map(getGroupByAlias),
247
+ ...((_b = query.select) !== null && _b !== void 0 ? _b : []).map(getSelectAlias),
248
+ ...((_c = query.calcs) !== null && _c !== void 0 ? _c : []).map((item) => item.as),
249
+ ].filter(Boolean);
250
+ return Array.from(new Set(selectColumns.length ? selectColumns : Object.keys((_d = rows[0]) !== null && _d !== void 0 ? _d : {})));
251
+ }
252
+ function getDefaultSelect(rows) {
253
+ var _a;
254
+ return Object.keys((_a = rows[0]) !== null && _a !== void 0 ? _a : {}).map((field) => ({ field }));
255
+ }
256
+ function isAggregateQuery(query) {
257
+ var _a, _b;
258
+ return Boolean(((_a = query.groupBy) === null || _a === void 0 ? void 0 : _a.length)
259
+ || ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
260
+ }
261
+ function isFieldSelectItem(item) {
262
+ return 'field' in item && !('agg' in item);
263
+ }
264
+ function isAggregateSelectItem(item) {
265
+ return 'agg' in item;
266
+ }
267
+ function isCalcSelectItem(item) {
268
+ return 'calc' in item;
269
+ }
270
+ function getSelectAlias(item) {
271
+ var _a;
272
+ if (isFieldSelectItem(item)) {
273
+ return (_a = item.as) !== null && _a !== void 0 ? _a : item.field;
274
+ }
275
+ return item.as;
276
+ }
277
+ function getGroupByField(item) {
278
+ return typeof item === 'string' ? item : item.field;
279
+ }
280
+ function getGroupByAlias(item) {
281
+ var _a;
282
+ return typeof item === 'string' ? item : (_a = item.as) !== null && _a !== void 0 ? _a : item.field;
283
+ }
284
+ function getGroupByGrain(item) {
285
+ return typeof item === 'string' ? undefined : item.grain;
286
+ }
287
+ function formatGroupValue(value, grain) {
288
+ if (!grain) {
289
+ return value;
290
+ }
291
+ const date = new Date(String(value));
292
+ if (!Number.isFinite(date.getTime())) {
293
+ return value;
294
+ }
295
+ if (grain === 'year') {
296
+ return `${date.getUTCFullYear()}`;
297
+ }
298
+ if (grain === 'quarter') {
299
+ return `${date.getUTCFullYear()}-Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
121
300
  }
301
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
302
+ if (grain === 'month') {
303
+ return `${date.getUTCFullYear()}-${month}`;
304
+ }
305
+ const day = String(date.getUTCDate()).padStart(2, '0');
306
+ if (grain === 'week') {
307
+ const weekStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
308
+ weekStart.setUTCDate(weekStart.getUTCDate() - weekStart.getUTCDay());
309
+ return weekStart.toISOString().slice(0, 10);
310
+ }
311
+ if (grain === 'day') {
312
+ return `${date.getUTCFullYear()}-${month}-${day}`;
313
+ }
314
+ const hour = String(date.getUTCHours()).padStart(2, '0');
315
+ return `${date.getUTCFullYear()}-${month}-${day}T${hour}:00:00.000Z`;
122
316
  }
123
- function createGroupByRule(rule) {
124
- if (rule.type === 'field') {
125
- return GroupBy.Field(rule.field);
317
+ function normalizeFilters(filters) {
318
+ if (Array.isArray(filters)) {
319
+ return filters.map((filter) => normalizeFilterNode(filter));
126
320
  }
127
- return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
321
+ if (isRecord(filters)) {
322
+ return normalizeFilterNode(filters);
323
+ }
324
+ return [];
325
+ }
326
+ function normalizeFilterNode(filter) {
327
+ if (!isRecord(filter)) {
328
+ return filter;
329
+ }
330
+ if (Array.isArray(filter.and)) {
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;
363
+ }
364
+ function matchesFilterExpression(row, filter) {
365
+ var _a, _b, _c, _d;
366
+ if (Array.isArray(filter)) {
367
+ return filter.every((item) => matchesFilterExpression(row, item));
368
+ }
369
+ if ('and' in filter) {
370
+ return filter.and.every((item) => matchesFilterExpression(row, item));
371
+ }
372
+ if ('or' in filter) {
373
+ return filter.or.some((item) => matchesFilterExpression(row, item));
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;
390
+ }
391
+ if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
392
+ return compareComparableValues(value, normalizeFilterValue(filter.lte)) <= 0;
393
+ }
394
+ if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
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);
429
+ }
430
+ return date.toISOString();
431
+ }
432
+ function toFiniteNumber(value) {
433
+ const numberValue = typeof value === 'number' ? value : Number(value);
434
+ return Number.isFinite(numberValue) ? numberValue : 0;
128
435
  }
129
436
  function isRecord(value) {
130
437
  return typeof value === 'object' && value !== null;
@@ -1,12 +1,35 @@
1
- import type { IHttpServer } from 'adminforth';
2
- import { DashboardApiResponseSchema, SlugRequestSchema } from '../schema/api.js';
3
- import type { DashboardRecord } from '../services/dashboardConfigService.js';
1
+ import type { AdminUser, IHttpServer } from 'adminforth';
2
+ import { normalizeDashboardConfig } from '../custom/model/dashboard.types.js';
3
+ import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
4
+ import {
5
+ DashboardApiResponseSchema,
6
+ DashboardConfigZodSchema,
7
+ SetDashboardConfigRequestSchema,
8
+ SlugRequestSchema,
9
+ } from '../schema/api.js';
10
+ import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
11
+ import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
4
12
  import { buildDashboardResponse } from '../services/dashboardConfigService.js';
5
13
 
6
14
  type DashboardEndpointsContext = {
15
+ canEditDashboard: (adminUser: AdminUser) => boolean;
7
16
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
17
+ persistDashboardConfig: (
18
+ dashboard: DashboardRecord,
19
+ config: DashboardConfig,
20
+ ) => Promise<PersistedDashboardResponse>;
21
+ validateDashboardWidgetApiConfig: (
22
+ widget: DashboardWidgetConfig,
23
+ ) => DashboardWidgetConfigValidationError[];
8
24
  };
9
25
 
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
+
10
33
  export function registerDashboardEndpoints(
11
34
  server: IHttpServer,
12
35
  ctx: DashboardEndpointsContext,
@@ -29,4 +52,54 @@ export function registerDashboardEndpoints(
29
52
  return buildDashboardResponse(dashboard);
30
53
  },
31
54
  });
32
- }
55
+
56
+ server.endpoint({
57
+ method: 'POST',
58
+ path: '/dashboard/set_dashboard_config',
59
+ description: 'Replaces one dashboard configuration, including groups and widgets. Superadmin only.',
60
+ request_schema: SetDashboardConfigRequestSchema,
61
+ response_schema: DashboardApiResponseSchema,
62
+ handler: async ({ body, adminUser, response }) => {
63
+ if (!ctx.canEditDashboard(adminUser)) {
64
+ response.setStatus(403);
65
+ return { error: 'Dashboard edit is not allowed' };
66
+ }
67
+
68
+ const slug = String(body?.slug || 'default');
69
+ const dashboard = await ctx.getDashboardRecord(slug);
70
+
71
+ if (!dashboard) {
72
+ response.setStatus(404);
73
+ return { error: 'Dashboard not found' };
74
+ }
75
+
76
+ const normalizedConfig = normalizeDashboardConfig(body?.config);
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
+ }
101
+
102
+ return ctx.persistDashboardConfig(dashboard, parsedConfig.data as DashboardConfig);
103
+ },
104
+ });
105
+ }
@@ -3,7 +3,7 @@ import { randomUUID } from 'crypto';
3
3
  import {
4
4
  normalizeDashboardWidgetConfig,
5
5
  } from '../custom/model/dashboard.types.js';
6
- import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
6
+ import type { DashboardConfig, DashboardVariables, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
7
7
  import {
8
8
  DashboardApiResponseSchema,
9
9
  DashboardWidgetDataResponseSchema,
@@ -31,7 +31,10 @@ type WidgetEndpointsContext = {
31
31
  ) => DashboardWidgetConfigValidationError[];
32
32
  getWidgetData: (
33
33
  widget: DashboardWidgetConfig,
34
- options?: { pagination?: { page: number, pageSize: number } },
34
+ options?: {
35
+ pagination?: { page: number, pageSize: number },
36
+ variables?: DashboardVariables,
37
+ },
35
38
  ) => Promise<unknown>;
36
39
  };
37
40
 
@@ -53,10 +56,13 @@ function formatWidgetConfigFieldPath(field: string) {
53
56
  const fieldAliases = new Map([
54
57
  ['minWidth', 'min_width'],
55
58
  ['maxWidth', 'max_width'],
56
- ['dataSource', 'data_source'],
57
- ['resourceId', 'resource_id'],
58
59
  ['groupBy', 'group_by'],
60
+ ['orderBy', 'order_by'],
59
61
  ['pageSize', 'page_size'],
62
+ ['timeSeries', 'time_series'],
63
+ ['valueField', 'value_field'],
64
+ ['targetValue', 'target_value'],
65
+ ['targetField', 'target_field'],
60
66
  ]);
61
67
 
62
68
  return field
@@ -298,6 +304,7 @@ export function registerWidgetEndpoints(
298
304
  widget,
299
305
  data: await ctx.getWidgetData(widget, {
300
306
  pagination: body?.pagination,
307
+ variables: widget.variables,
301
308
  }),
302
309
  };
303
310
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/dashboard",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",