@adminforth/dashboard 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -39
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +327 -234
- package/custom/skills/adminforth-dashboard/SKILL.md +6 -2
- package/custom/widgets/chart/ChartWidget.vue +23 -55
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +17 -66
- package/custom/widgets/chart/chart.utils.ts +11 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/custom/widgets/chart/line/LineChart.vue +23 -15
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +178 -61
- package/dist/custom/model/dashboard.types.js +67 -92
- package/dist/custom/model/dashboard.types.ts +327 -234
- package/dist/custom/queries/useDashboardConfig.d.ts +832 -66
- package/dist/custom/queries/useWidgetData.d.ts +828 -62
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +6 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +23 -55
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
- package/dist/custom/widgets/chart/chart.types.js +2 -25
- package/dist/custom/widgets/chart/chart.types.ts +17 -66
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
- package/dist/custom/widgets/chart/chart.utils.js +7 -0
- package/dist/custom/widgets/chart/chart.utils.ts +11 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/dist/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/endpoint/widgets.js +5 -2
- package/dist/schema/api.d.ts +2680 -736
- package/dist/schema/widget.d.ts +1588 -476
- package/dist/schema/widget.js +205 -139
- package/dist/services/widgetConfigValidator.js +16 -40
- package/dist/services/widgetDataService.js +359 -82
- package/endpoint/dashboard.ts +1 -1
- package/endpoint/widgets.ts +5 -2
- package/package.json +1 -1
- package/schema/widget.ts +222 -139
- package/services/widgetConfigValidator.ts +29 -53
- package/services/widgetDataService.ts +484 -100
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Filters, Sorts } from 'adminforth';
|
|
2
2
|
import type {
|
|
3
3
|
IAdminForth,
|
|
4
4
|
IAdminForthAndOrFilter,
|
|
@@ -6,11 +6,19 @@ import type {
|
|
|
6
6
|
IAdminForthSort,
|
|
7
7
|
} from 'adminforth';
|
|
8
8
|
import type {
|
|
9
|
-
AggregationRule,
|
|
10
9
|
DashboardWidgetConfig,
|
|
11
10
|
DashboardWidgetData,
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
FilterExpression,
|
|
12
|
+
FunnelQueryConfig,
|
|
13
|
+
QueryAggregateOperation,
|
|
14
|
+
QueryAggregateSelectItem,
|
|
15
|
+
QueryCalcSelectItem,
|
|
16
|
+
QueryConfig,
|
|
17
|
+
QueryFieldSelectItem,
|
|
18
|
+
QueryGroupByItem,
|
|
19
|
+
QueryOrderByItem,
|
|
20
|
+
QuerySelectItem,
|
|
21
|
+
TimeGrain,
|
|
14
22
|
} from '../custom/model/dashboard.types.js';
|
|
15
23
|
|
|
16
24
|
export type DashboardWidgetDataOptions = {
|
|
@@ -25,6 +33,15 @@ type DashboardWidgetFilters =
|
|
|
25
33
|
| IAdminForthAndOrFilter
|
|
26
34
|
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
27
35
|
|
|
36
|
+
type QueryRowGroup = {
|
|
37
|
+
rows: Record<string, unknown>[];
|
|
38
|
+
values: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const NOW_MINUS_RE = /^(\d+)([dhw])$/;
|
|
42
|
+
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
43
|
+
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
44
|
+
|
|
28
45
|
export type WidgetDataService = {
|
|
29
46
|
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
30
47
|
};
|
|
@@ -34,163 +51,530 @@ export async function getWidgetData(
|
|
|
34
51
|
widget: DashboardWidgetConfig,
|
|
35
52
|
options: DashboardWidgetDataOptions = {},
|
|
36
53
|
): Promise<DashboardWidgetData | null> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!dataSource) {
|
|
54
|
+
if (!('query' in widget)) {
|
|
40
55
|
return null;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
const data = 'steps' in widget.query
|
|
59
|
+
? await getFunnelWidgetData(adminforth, widget.query)
|
|
60
|
+
: await getQueryWidgetData(adminforth, widget.query);
|
|
61
|
+
|
|
62
|
+
if (widget.target !== 'table' || !options.pagination) {
|
|
63
|
+
return data;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
const page = options.pagination.page;
|
|
67
|
+
const pageSize = options.pagination.pageSize;
|
|
68
|
+
const offset = (page - 1) * pageSize;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...data,
|
|
72
|
+
rows: data.rows.slice(offset, offset + pageSize),
|
|
73
|
+
pagination: {
|
|
74
|
+
page,
|
|
75
|
+
pageSize,
|
|
76
|
+
total: data.rows.length,
|
|
77
|
+
totalPages: Math.max(Math.ceil(data.rows.length / pageSize), 1),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
48
80
|
}
|
|
49
81
|
|
|
50
|
-
async function
|
|
82
|
+
async function getFunnelWidgetData(
|
|
51
83
|
adminforth: IAdminForth,
|
|
52
|
-
|
|
53
|
-
options: DashboardWidgetDataOptions,
|
|
84
|
+
query: FunnelQueryConfig,
|
|
54
85
|
): Promise<DashboardWidgetData> {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const pagination = options.pagination;
|
|
59
|
-
const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
|
|
60
|
-
const limit = pagination ? pagination.pageSize : undefined;
|
|
61
|
-
|
|
62
|
-
const rows = await resource.list(
|
|
63
|
-
filters,
|
|
64
|
-
limit ?? undefined,
|
|
65
|
-
offset,
|
|
66
|
-
sort,
|
|
67
|
-
);
|
|
86
|
+
const rows = await Promise.all(query.steps.map(async (step) => {
|
|
87
|
+
const valueField = step.metric.as;
|
|
88
|
+
const sourceRows = await getResourceRows(adminforth, step.resource, step.filters);
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
return {
|
|
91
|
+
name: step.name,
|
|
92
|
+
[valueField]: calculateAggregate(sourceRows, step.metric),
|
|
93
|
+
};
|
|
94
|
+
}));
|
|
71
95
|
|
|
72
96
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)),
|
|
77
|
-
...(pagination ? {
|
|
78
|
-
pagination: {
|
|
79
|
-
page: pagination.page,
|
|
80
|
-
pageSize: pagination.pageSize,
|
|
81
|
-
total,
|
|
82
|
-
totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
|
|
83
|
-
},
|
|
84
|
-
} : {}),
|
|
97
|
+
kind: 'aggregate',
|
|
98
|
+
columns: ['name', ...Array.from(new Set(query.steps.map((step) => step.metric.as)))],
|
|
99
|
+
rows,
|
|
85
100
|
};
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
async function
|
|
103
|
+
async function getQueryWidgetData(
|
|
89
104
|
adminforth: IAdminForth,
|
|
90
|
-
|
|
105
|
+
query: QueryConfig,
|
|
91
106
|
): Promise<DashboardWidgetData> {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
]),
|
|
100
|
-
),
|
|
101
|
-
dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined,
|
|
102
|
-
);
|
|
103
|
-
const columns = Object.keys(rows[0] ?? {});
|
|
107
|
+
const rows = await getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.orderBy));
|
|
108
|
+
const selectedRows = buildQueryRows(rows, query);
|
|
109
|
+
const orderedRows = sortRows(selectedRows, query.orderBy);
|
|
110
|
+
const slicedRows = typeof query.limit === 'number'
|
|
111
|
+
? orderedRows.slice(query.offset ?? 0, (query.offset ?? 0) + query.limit)
|
|
112
|
+
: orderedRows.slice(query.offset ?? 0);
|
|
113
|
+
const columns = getColumns(slicedRows, query);
|
|
104
114
|
|
|
105
|
-
if (
|
|
106
|
-
const values =
|
|
115
|
+
if (isAggregateQuery(query)) {
|
|
116
|
+
const values = slicedRows.length === 1 ? slicedRows[0] : undefined;
|
|
107
117
|
|
|
108
118
|
return {
|
|
109
119
|
kind: 'aggregate',
|
|
110
|
-
columns
|
|
111
|
-
rows:
|
|
112
|
-
values,
|
|
120
|
+
columns,
|
|
121
|
+
rows: slicedRows,
|
|
122
|
+
...(values ? { values } : {}),
|
|
113
123
|
};
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
return {
|
|
117
|
-
kind: '
|
|
127
|
+
kind: 'table',
|
|
118
128
|
columns,
|
|
119
|
-
rows,
|
|
129
|
+
rows: slicedRows,
|
|
120
130
|
};
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
async function getResourceRows(
|
|
134
|
+
adminforth: IAdminForth,
|
|
135
|
+
resourceId: string,
|
|
136
|
+
filters: unknown,
|
|
137
|
+
sort?: IAdminForthSort | IAdminForthSort[],
|
|
138
|
+
) {
|
|
139
|
+
return adminforth.resource(resourceId).list(
|
|
140
|
+
normalizeFilters(filters),
|
|
141
|
+
undefined,
|
|
142
|
+
0,
|
|
143
|
+
sort,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildQueryRows(rows: Record<string, unknown>[], query: QueryConfig) {
|
|
148
|
+
const select = query.select ?? getDefaultSelect(rows);
|
|
149
|
+
const groupBy = query.groupBy ?? [];
|
|
150
|
+
|
|
151
|
+
if (isAggregateQuery(query)) {
|
|
152
|
+
return buildGroupedRows(rows, select, groupBy, query.calcs);
|
|
126
153
|
}
|
|
127
154
|
|
|
128
|
-
return
|
|
155
|
+
return rows.map((row) => buildPlainRow(row, select, query.calcs));
|
|
129
156
|
}
|
|
130
157
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
function buildGroupedRows(
|
|
159
|
+
rows: Record<string, unknown>[],
|
|
160
|
+
select: QuerySelectItem[],
|
|
161
|
+
groupBy: QueryGroupByItem[],
|
|
162
|
+
calcs: QueryCalcSelectItem[] = [],
|
|
163
|
+
) {
|
|
164
|
+
const groups = new Map<string, QueryRowGroup>();
|
|
165
|
+
const effectiveGroupBy = groupBy.length
|
|
166
|
+
? groupBy
|
|
167
|
+
: select.filter(isFieldSelectItem).map((item) => ({ field: item.field, as: item.as, grain: item.grain }));
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
169
|
+
if (!effectiveGroupBy.length) {
|
|
170
|
+
const values = calculateGroupValues(rows, select, calcs);
|
|
171
|
+
return Object.keys(values).length ? [values] : [];
|
|
140
172
|
}
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const values = Object.fromEntries(effectiveGroupBy.map((item) => {
|
|
176
|
+
const field = getGroupByField(item);
|
|
177
|
+
const alias = getGroupByAlias(item);
|
|
178
|
+
const grain = getGroupByGrain(item);
|
|
179
|
+
|
|
180
|
+
return [alias, formatGroupValue(row[field], grain)];
|
|
181
|
+
}));
|
|
182
|
+
const key = JSON.stringify(values);
|
|
183
|
+
const group = groups.get(key) ?? { values, rows: [] };
|
|
184
|
+
|
|
185
|
+
group.rows.push(row);
|
|
186
|
+
groups.set(key, group);
|
|
144
187
|
}
|
|
145
188
|
|
|
146
|
-
return
|
|
189
|
+
return Array.from(groups.values()).map((group) => ({
|
|
190
|
+
...group.values,
|
|
191
|
+
...calculateGroupValues(group.rows, select, calcs),
|
|
192
|
+
}));
|
|
147
193
|
}
|
|
148
194
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
function calculateGroupValues(
|
|
196
|
+
rows: Record<string, unknown>[],
|
|
197
|
+
select: QuerySelectItem[],
|
|
198
|
+
calcs: QueryCalcSelectItem[],
|
|
199
|
+
) {
|
|
200
|
+
const values: Record<string, unknown> = {};
|
|
201
|
+
|
|
202
|
+
for (const item of select) {
|
|
203
|
+
if (isAggregateSelectItem(item)) {
|
|
204
|
+
const filteredRows = item.filters
|
|
205
|
+
? rows.filter((row) => matchesFilterExpression(row, item.filters as FilterExpression))
|
|
206
|
+
: rows;
|
|
207
|
+
|
|
208
|
+
values[item.as] = calculateAggregate(filteredRows, item);
|
|
209
|
+
}
|
|
152
210
|
}
|
|
153
211
|
|
|
154
|
-
|
|
155
|
-
|
|
212
|
+
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
213
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
156
214
|
}
|
|
157
215
|
|
|
158
|
-
|
|
159
|
-
|
|
216
|
+
return values;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildPlainRow(
|
|
220
|
+
row: Record<string, unknown>,
|
|
221
|
+
select: QuerySelectItem[],
|
|
222
|
+
calcs: QueryCalcSelectItem[] = [],
|
|
223
|
+
) {
|
|
224
|
+
const values: Record<string, unknown> = {};
|
|
225
|
+
|
|
226
|
+
for (const item of select) {
|
|
227
|
+
if (isFieldSelectItem(item)) {
|
|
228
|
+
values[item.as ?? item.field] = item.grain
|
|
229
|
+
? formatGroupValue(row[item.field], item.grain)
|
|
230
|
+
: row[item.field];
|
|
231
|
+
}
|
|
160
232
|
}
|
|
161
233
|
|
|
162
|
-
|
|
163
|
-
|
|
234
|
+
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
235
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
164
236
|
}
|
|
165
237
|
|
|
166
|
-
return
|
|
238
|
+
return values;
|
|
167
239
|
}
|
|
168
240
|
|
|
169
|
-
function
|
|
170
|
-
switch (
|
|
171
|
-
case 'sum':
|
|
172
|
-
return Aggregates.sum(rule.field!);
|
|
241
|
+
function calculateAggregate(rows: Record<string, unknown>[], item: QueryAggregateSelectItem) {
|
|
242
|
+
switch (item.agg) {
|
|
173
243
|
case 'count':
|
|
174
|
-
return
|
|
244
|
+
return rows.length;
|
|
245
|
+
case 'count_distinct':
|
|
246
|
+
return new Set(rows.map((row) => row[item.field!])).size;
|
|
247
|
+
case 'sum':
|
|
248
|
+
return aggregateNumbers(rows, item.field!, (values) => values.reduce((sum, value) => sum + value, 0));
|
|
175
249
|
case 'avg':
|
|
176
|
-
return
|
|
250
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length
|
|
251
|
+
? values.reduce((sum, value) => sum + value, 0) / values.length
|
|
252
|
+
: 0);
|
|
177
253
|
case 'min':
|
|
178
|
-
return
|
|
254
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.min(...values) : 0);
|
|
179
255
|
case 'max':
|
|
180
|
-
return
|
|
256
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.max(...values) : 0);
|
|
181
257
|
case 'median':
|
|
182
|
-
return
|
|
258
|
+
return aggregateNumbers(rows, item.field!, calculateMedian);
|
|
183
259
|
default:
|
|
184
|
-
throw new Error(`Unsupported aggregation operation: ${(
|
|
260
|
+
throw new Error(`Unsupported aggregation operation: ${(item as { agg: QueryAggregateOperation }).agg}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function aggregateNumbers(
|
|
265
|
+
rows: Record<string, unknown>[],
|
|
266
|
+
field: string,
|
|
267
|
+
aggregate: (values: number[]) => number,
|
|
268
|
+
) {
|
|
269
|
+
return aggregate(rows.map((row) => toFiniteNumber(row[field])).filter(Number.isFinite));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function calculateMedian(values: number[]) {
|
|
273
|
+
if (!values.length) {
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
278
|
+
const middle = Math.floor(sorted.length / 2);
|
|
279
|
+
|
|
280
|
+
return sorted.length % 2
|
|
281
|
+
? sorted[middle]
|
|
282
|
+
: (sorted[middle - 1] + sorted[middle]) / 2;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function evaluateCalc(calc: string, values: Record<string, unknown>) {
|
|
286
|
+
const expression = calc.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
287
|
+
|
|
288
|
+
if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
|
|
289
|
+
throw new Error(`Unsupported calc expression: ${calc}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return Function(`"use strict"; return (${expression});`)();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sortRows(rows: Record<string, unknown>[], orderBy: QueryOrderByItem[] = []) {
|
|
296
|
+
if (!orderBy.length) {
|
|
297
|
+
return rows;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return [...rows].sort((left, right) => {
|
|
301
|
+
for (const order of orderBy) {
|
|
302
|
+
const direction = order.direction === 'asc' ? 1 : -1;
|
|
303
|
+
const result = compareValues(left[order.field], right[order.field]);
|
|
304
|
+
|
|
305
|
+
if (result !== 0) {
|
|
306
|
+
return result * direction;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return 0;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function compareValues(left: unknown, right: unknown) {
|
|
315
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
316
|
+
return left - right;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return String(left ?? '').localeCompare(String(right ?? ''));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getBackendSort(orderBy: QueryOrderByItem[] | undefined) {
|
|
323
|
+
if (!orderBy?.length) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return orderBy.map((order) => order.direction === 'asc'
|
|
328
|
+
? Sorts.ASC(order.field)
|
|
329
|
+
: Sorts.DESC(order.field));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getColumns(rows: Record<string, unknown>[], query: QueryConfig) {
|
|
333
|
+
const selectColumns = [
|
|
334
|
+
...(query.groupBy ?? []).map(getGroupByAlias),
|
|
335
|
+
...(query.select ?? []).map(getSelectAlias),
|
|
336
|
+
...(query.calcs ?? []).map((item) => item.as),
|
|
337
|
+
].filter(Boolean);
|
|
338
|
+
|
|
339
|
+
return Array.from(new Set(selectColumns.length ? selectColumns : Object.keys(rows[0] ?? {})));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getDefaultSelect(rows: Record<string, unknown>[]): QuerySelectItem[] {
|
|
343
|
+
return Object.keys(rows[0] ?? {}).map((field) => ({ field }));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isAggregateQuery(query: QueryConfig) {
|
|
347
|
+
return Boolean(
|
|
348
|
+
query.groupBy?.length
|
|
349
|
+
|| query.select?.some((item) => isAggregateSelectItem(item)),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isFieldSelectItem(item: QuerySelectItem): item is QueryFieldSelectItem {
|
|
354
|
+
return 'field' in item && !('agg' in item);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isAggregateSelectItem(item: QuerySelectItem): item is QueryAggregateSelectItem {
|
|
358
|
+
return 'agg' in item;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isCalcSelectItem(item: QuerySelectItem): item is QueryCalcSelectItem {
|
|
362
|
+
return 'calc' in item;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function getSelectAlias(item: QuerySelectItem) {
|
|
366
|
+
if (isFieldSelectItem(item)) {
|
|
367
|
+
return item.as ?? item.field;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return item.as;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getGroupByField(item: QueryGroupByItem) {
|
|
374
|
+
return typeof item === 'string' ? item : item.field;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getGroupByAlias(item: QueryGroupByItem) {
|
|
378
|
+
return typeof item === 'string' ? item : item.as ?? item.field;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getGroupByGrain(item: QueryGroupByItem) {
|
|
382
|
+
return typeof item === 'string' ? undefined : item.grain;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
386
|
+
if (!grain) {
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const date = new Date(String(value));
|
|
391
|
+
|
|
392
|
+
if (!Number.isFinite(date.getTime())) {
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (grain === 'year') {
|
|
397
|
+
return `${date.getUTCFullYear()}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (grain === 'quarter') {
|
|
401
|
+
return `${date.getUTCFullYear()}-Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
405
|
+
|
|
406
|
+
if (grain === 'month') {
|
|
407
|
+
return `${date.getUTCFullYear()}-${month}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
411
|
+
|
|
412
|
+
if (grain === 'week') {
|
|
413
|
+
const weekStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
414
|
+
weekStart.setUTCDate(weekStart.getUTCDate() - weekStart.getUTCDay());
|
|
415
|
+
return weekStart.toISOString().slice(0, 10);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (grain === 'day') {
|
|
419
|
+
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const hour = String(date.getUTCHours()).padStart(2, '0');
|
|
423
|
+
return `${date.getUTCFullYear()}-${month}-${day}T${hour}:00:00.000Z`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeFilters(filters: unknown): DashboardWidgetFilters {
|
|
427
|
+
if (Array.isArray(filters)) {
|
|
428
|
+
return filters.map((filter) => normalizeFilterNode(filter)) as DashboardWidgetFilters;
|
|
185
429
|
}
|
|
430
|
+
|
|
431
|
+
if (isRecord(filters)) {
|
|
432
|
+
return normalizeFilterNode(filters);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return [];
|
|
186
436
|
}
|
|
187
437
|
|
|
188
|
-
function
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
438
|
+
function normalizeFilterNode(filter: unknown): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
439
|
+
if (!isRecord(filter)) {
|
|
440
|
+
return filter as IAdminForthSingleFilter;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (Array.isArray(filter.and)) {
|
|
444
|
+
return Filters.AND(filter.and.map((item) => normalizeFilterNode(item)));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (Array.isArray(filter.or)) {
|
|
448
|
+
return Filters.OR(filter.or.map((item) => normalizeFilterNode(item)));
|
|
191
449
|
}
|
|
192
450
|
|
|
193
|
-
|
|
451
|
+
if (typeof filter.field === 'string') {
|
|
452
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
|
|
453
|
+
return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
457
|
+
return Filters.NEQ(filter.field, normalizeFilterValue(filter.neq));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
461
|
+
return Filters.GT(filter.field, normalizeFilterValue(filter.gt));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
465
|
+
return Filters.GTE(filter.field, normalizeFilterValue(filter.gte));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
469
|
+
return Filters.LT(filter.field, normalizeFilterValue(filter.lt));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
|
|
473
|
+
return Filters.LTE(filter.field, normalizeFilterValue(filter.lte));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
|
|
477
|
+
return Filters.IN(filter.field, normalizeFilterValue(filter.in));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
481
|
+
return Filters.NOT_IN(filter.field, normalizeFilterValue(filter.not_in));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return filter as IAdminForthSingleFilter | IAdminForthAndOrFilter;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function matchesFilterExpression(row: Record<string, unknown>, filter: FilterExpression): boolean {
|
|
489
|
+
if (Array.isArray(filter)) {
|
|
490
|
+
return filter.every((item) => matchesFilterExpression(row, item));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if ('and' in filter) {
|
|
494
|
+
return filter.and.every((item) => matchesFilterExpression(row, item));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if ('or' in filter) {
|
|
498
|
+
return filter.or.some((item) => matchesFilterExpression(row, item));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const value = row[filter.field];
|
|
502
|
+
|
|
503
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'eq')) {
|
|
504
|
+
return value === normalizeFilterValue(filter.eq);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'neq')) {
|
|
508
|
+
return value !== normalizeFilterValue(filter.neq);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'gt')) {
|
|
512
|
+
return compareComparableValues(value, normalizeFilterValue(filter.gt)) > 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'gte')) {
|
|
516
|
+
return compareComparableValues(value, normalizeFilterValue(filter.gte)) >= 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
520
|
+
return compareComparableValues(value, normalizeFilterValue(filter.lt)) < 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
|
|
524
|
+
return compareComparableValues(value, normalizeFilterValue(filter.lte)) <= 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'in')) {
|
|
528
|
+
return filter.in?.includes(value) ?? false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'not_in')) {
|
|
532
|
+
return !(filter.not_in?.includes(value) ?? false);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function compareComparableValues(left: unknown, right: unknown) {
|
|
539
|
+
const leftNumber = Number(left);
|
|
540
|
+
const rightNumber = Number(right);
|
|
541
|
+
|
|
542
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
543
|
+
return leftNumber - rightNumber;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return String(left ?? '').localeCompare(String(right ?? ''));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeFilterValue(value: unknown) {
|
|
550
|
+
if (!isRecord(value) || typeof value.now_minus !== 'string') {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const match = value.now_minus.match(NOW_MINUS_RE);
|
|
555
|
+
|
|
556
|
+
if (!match) {
|
|
557
|
+
return value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const amount = Number(match[1]);
|
|
561
|
+
const unit = match[2];
|
|
562
|
+
const date = new Date();
|
|
563
|
+
|
|
564
|
+
if (unit === 'h') {
|
|
565
|
+
date.setHours(date.getHours() - amount);
|
|
566
|
+
} else if (unit === 'w') {
|
|
567
|
+
date.setDate(date.getDate() - amount * 7);
|
|
568
|
+
} else {
|
|
569
|
+
date.setDate(date.getDate() - amount);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return date.toISOString();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function toFiniteNumber(value: unknown) {
|
|
576
|
+
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
577
|
+
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
194
578
|
}
|
|
195
579
|
|
|
196
580
|
function isRecord(value: unknown): value is Record<string, any> {
|