@adminforth/dashboard 1.1.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 +43 -52
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +385 -98
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/custom/widgets/chart/ChartWidget.vue +36 -35
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +42 -8
- 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 -43
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +179 -38
- package/dist/custom/model/dashboard.types.js +108 -42
- package/dist/custom/model/dashboard.types.ts +385 -98
- package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
- package/dist/custom/queries/useWidgetData.d.ts +828 -64
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
- package/dist/custom/widgets/chart/chart.types.js +23 -0
- package/dist/custom/widgets/chart/chart.types.ts +42 -8
- 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 -43
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/dist/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/endpoint/widgets.js +23 -3
- package/dist/schema/api.d.ts +2637 -933
- package/dist/schema/widget.d.ts +1562 -582
- package/dist/schema/widget.js +207 -127
- package/dist/services/widgetConfigValidator.js +16 -80
- package/dist/services/widgetDataService.d.ts +0 -9
- package/dist/services/widgetDataService.js +356 -97
- package/endpoint/dashboard.ts +1 -1
- package/endpoint/widgets.ts +29 -3
- package/package.json +1 -1
- package/schema/widget.ts +221 -121
- package/services/widgetConfigValidator.ts +29 -100
- package/services/widgetDataService.ts +478 -129
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Filters, Sorts } from 'adminforth';
|
|
2
2
|
import type {
|
|
3
3
|
IAdminForth,
|
|
4
4
|
IAdminForthAndOrFilter,
|
|
@@ -6,23 +6,21 @@ 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
|
-
export type DashboardWidgetQueryConfig = {
|
|
17
|
-
resource: string;
|
|
18
|
-
select?: string[];
|
|
19
|
-
order?: {
|
|
20
|
-
field: string;
|
|
21
|
-
direction: 'asc' | 'desc';
|
|
22
|
-
};
|
|
23
|
-
limit?: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
24
|
export type DashboardWidgetDataOptions = {
|
|
27
25
|
pagination?: {
|
|
28
26
|
page: number;
|
|
@@ -35,6 +33,15 @@ type DashboardWidgetFilters =
|
|
|
35
33
|
| IAdminForthAndOrFilter
|
|
36
34
|
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
37
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
|
+
|
|
38
45
|
export type WidgetDataService = {
|
|
39
46
|
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
40
47
|
};
|
|
@@ -44,188 +51,530 @@ export async function getWidgetData(
|
|
|
44
51
|
widget: DashboardWidgetConfig,
|
|
45
52
|
options: DashboardWidgetDataOptions = {},
|
|
46
53
|
): Promise<DashboardWidgetData | null> {
|
|
47
|
-
|
|
48
|
-
const dataSource = getWidgetDataSource(widget, legacyQuery);
|
|
49
|
-
|
|
50
|
-
if (!dataSource) {
|
|
54
|
+
if (!('query' in widget)) {
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
|
|
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
|
+
};
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
async function
|
|
82
|
+
async function getFunnelWidgetData(
|
|
62
83
|
adminforth: IAdminForth,
|
|
63
|
-
|
|
64
|
-
legacyQuery: DashboardWidgetQueryConfig | undefined,
|
|
65
|
-
options: DashboardWidgetDataOptions,
|
|
84
|
+
query: FunnelQueryConfig,
|
|
66
85
|
): Promise<DashboardWidgetData> {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const pagination = options.pagination;
|
|
71
|
-
const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
|
|
72
|
-
const queryLimit = legacyQuery?.limit;
|
|
73
|
-
const limit = pagination
|
|
74
|
-
? Math.max(Math.min(pagination.pageSize, (queryLimit ?? Infinity) - offset), 0)
|
|
75
|
-
: queryLimit;
|
|
76
|
-
|
|
77
|
-
const rows = await resource.list(
|
|
78
|
-
filters,
|
|
79
|
-
limit ?? undefined,
|
|
80
|
-
offset,
|
|
81
|
-
sort,
|
|
82
|
-
);
|
|
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);
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
return {
|
|
91
|
+
name: step.name,
|
|
92
|
+
[valueField]: calculateAggregate(sourceRows, step.metric),
|
|
93
|
+
};
|
|
94
|
+
}));
|
|
86
95
|
|
|
87
96
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)),
|
|
92
|
-
...(pagination ? {
|
|
93
|
-
pagination: {
|
|
94
|
-
page: pagination.page,
|
|
95
|
-
pageSize: pagination.pageSize,
|
|
96
|
-
total,
|
|
97
|
-
totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
|
|
98
|
-
},
|
|
99
|
-
} : {}),
|
|
97
|
+
kind: 'aggregate',
|
|
98
|
+
columns: ['name', ...Array.from(new Set(query.steps.map((step) => step.metric.as)))],
|
|
99
|
+
rows,
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async function
|
|
103
|
+
async function getQueryWidgetData(
|
|
104
104
|
adminforth: IAdminForth,
|
|
105
|
-
|
|
105
|
+
query: QueryConfig,
|
|
106
106
|
): Promise<DashboardWidgetData> {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
]),
|
|
115
|
-
),
|
|
116
|
-
dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined,
|
|
117
|
-
);
|
|
118
|
-
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);
|
|
119
114
|
|
|
120
|
-
if (
|
|
121
|
-
const values =
|
|
115
|
+
if (isAggregateQuery(query)) {
|
|
116
|
+
const values = slicedRows.length === 1 ? slicedRows[0] : undefined;
|
|
122
117
|
|
|
123
118
|
return {
|
|
124
119
|
kind: 'aggregate',
|
|
125
|
-
columns
|
|
126
|
-
rows:
|
|
127
|
-
values,
|
|
120
|
+
columns,
|
|
121
|
+
rows: slicedRows,
|
|
122
|
+
...(values ? { values } : {}),
|
|
128
123
|
};
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
return {
|
|
132
|
-
kind: '
|
|
127
|
+
kind: 'table',
|
|
133
128
|
columns,
|
|
134
|
-
rows,
|
|
129
|
+
rows: slicedRows,
|
|
135
130
|
};
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
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);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
return
|
|
155
|
+
return rows.map((row) => buildPlainRow(row, select, query.calcs));
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 }));
|
|
168
|
+
|
|
169
|
+
if (!effectiveGroupBy.length) {
|
|
170
|
+
const values = calculateGroupValues(rows, select, calcs);
|
|
171
|
+
return Object.keys(values).length ? [values] : [];
|
|
172
|
+
}
|
|
173
|
+
|
|
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);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Array.from(groups.values()).map((group) => ({
|
|
190
|
+
...group.values,
|
|
191
|
+
...calculateGroupValues(group.rows, select, calcs),
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
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
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
213
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
214
|
+
}
|
|
215
|
+
|
|
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
|
+
}
|
|
152
232
|
}
|
|
153
233
|
|
|
154
|
-
|
|
234
|
+
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
235
|
+
values[item.as] = evaluateCalc(item.calc, values);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return values;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function calculateAggregate(rows: Record<string, unknown>[], item: QueryAggregateSelectItem) {
|
|
242
|
+
switch (item.agg) {
|
|
243
|
+
case 'count':
|
|
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));
|
|
249
|
+
case 'avg':
|
|
250
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length
|
|
251
|
+
? values.reduce((sum, value) => sum + value, 0) / values.length
|
|
252
|
+
: 0);
|
|
253
|
+
case 'min':
|
|
254
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.min(...values) : 0);
|
|
255
|
+
case 'max':
|
|
256
|
+
return aggregateNumbers(rows, item.field!, (values) => values.length ? Math.max(...values) : 0);
|
|
257
|
+
case 'median':
|
|
258
|
+
return aggregateNumbers(rows, item.field!, calculateMedian);
|
|
259
|
+
default:
|
|
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) {
|
|
155
324
|
return undefined;
|
|
156
325
|
}
|
|
157
326
|
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 }));
|
|
164
344
|
}
|
|
165
345
|
|
|
166
|
-
function
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
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`;
|
|
170
424
|
}
|
|
171
425
|
|
|
172
426
|
function normalizeFilters(filters: unknown): DashboardWidgetFilters {
|
|
173
427
|
if (Array.isArray(filters)) {
|
|
174
|
-
return filters as DashboardWidgetFilters;
|
|
428
|
+
return filters.map((filter) => normalizeFilterNode(filter)) as DashboardWidgetFilters;
|
|
175
429
|
}
|
|
176
430
|
|
|
177
431
|
if (isRecord(filters)) {
|
|
178
|
-
return filters
|
|
432
|
+
return normalizeFilterNode(filters);
|
|
179
433
|
}
|
|
180
434
|
|
|
181
435
|
return [];
|
|
182
436
|
}
|
|
183
437
|
|
|
184
|
-
function
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
438
|
+
function normalizeFilterNode(filter: unknown): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
439
|
+
if (!isRecord(filter)) {
|
|
440
|
+
return filter as IAdminForthSingleFilter;
|
|
187
441
|
}
|
|
188
442
|
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
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)));
|
|
449
|
+
}
|
|
450
|
+
|
|
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;
|
|
191
517
|
}
|
|
192
518
|
|
|
193
|
-
if (
|
|
194
|
-
return
|
|
519
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lt')) {
|
|
520
|
+
return compareComparableValues(value, normalizeFilterValue(filter.lt)) < 0;
|
|
195
521
|
}
|
|
196
522
|
|
|
197
|
-
if (
|
|
198
|
-
return
|
|
523
|
+
if (Object.prototype.hasOwnProperty.call(filter, 'lte')) {
|
|
524
|
+
return compareComparableValues(value, normalizeFilterValue(filter.lte)) <= 0;
|
|
199
525
|
}
|
|
200
526
|
|
|
201
|
-
|
|
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;
|
|
202
536
|
}
|
|
203
537
|
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
case 'avg':
|
|
211
|
-
return Aggregates.avg(rule.field!);
|
|
212
|
-
case 'min':
|
|
213
|
-
return Aggregates.min(rule.field!);
|
|
214
|
-
case 'max':
|
|
215
|
-
return Aggregates.max(rule.field!);
|
|
216
|
-
case 'median':
|
|
217
|
-
return Aggregates.median(rule.field!);
|
|
218
|
-
default:
|
|
219
|
-
throw new Error(`Unsupported aggregation operation: ${(rule as AggregationRule).operation}`);
|
|
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;
|
|
220
544
|
}
|
|
545
|
+
|
|
546
|
+
return String(left ?? '').localeCompare(String(right ?? ''));
|
|
221
547
|
}
|
|
222
548
|
|
|
223
|
-
function
|
|
224
|
-
if (
|
|
225
|
-
return
|
|
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;
|
|
226
558
|
}
|
|
227
559
|
|
|
228
|
-
|
|
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;
|
|
229
578
|
}
|
|
230
579
|
|
|
231
580
|
function isRecord(value: unknown): value is Record<string, any> {
|