@adminforth/dashboard 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -15
- package/custom/api/dashboardApi.ts +9 -8
- package/custom/model/dashboard.types.ts +63 -270
- package/custom/model/dashboardTopics.ts +5 -0
- package/custom/runtime/DashboardGroup.vue +2 -2
- package/custom/runtime/DashboardPage.vue +17 -7
- package/custom/runtime/DashboardRuntime.vue +20 -8
- package/custom/runtime/WidgetRenderer.vue +1 -2
- package/custom/runtime/WidgetShell.vue +3 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{dist/custom/widgets/chart/funnel → custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{dist/custom/widgets/chart/line → custom/widgets/chart}/LineChart.vue +2 -2
- package/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{dist/custom/widgets/chart/stacked-bar → custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/custom/api/dashboardApi.d.ts +4 -7
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +9 -8
- package/dist/custom/model/dashboard.types.d.ts +40 -31
- package/dist/custom/model/dashboard.types.js +13 -152
- package/dist/custom/model/dashboard.types.ts +63 -270
- package/dist/custom/model/dashboardTopics.d.ts +2 -0
- package/dist/custom/model/dashboardTopics.js +8 -0
- package/dist/custom/model/dashboardTopics.ts +5 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +116 -96
- package/dist/custom/queries/useWidgetData.d.ts +116 -96
- package/dist/custom/runtime/DashboardGroup.vue +2 -2
- package/dist/custom/runtime/DashboardPage.vue +17 -7
- package/dist/custom/runtime/DashboardRuntime.vue +20 -8
- package/dist/custom/runtime/WidgetRenderer.vue +1 -2
- package/dist/custom/runtime/WidgetShell.vue +3 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +110 -3
- package/dist/custom/widgets/{gauge-card/GaugeCardWidget.vue → GaugeCardWidget.vue} +63 -61
- package/dist/custom/widgets/{kpi-card/KpiCardWidget.vue → KpiCardWidget.vue} +35 -33
- package/dist/custom/widgets/{pivot-table/PivotTableWidget.vue → PivotTableWidget.vue} +71 -68
- package/dist/custom/widgets/{table/TableWidget.vue → TableWidget.vue} +5 -5
- package/dist/custom/widgets/chart/{bar/BarChart.vue → BarChart.vue} +2 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +24 -18
- package/{custom/widgets/chart/funnel → dist/custom/widgets/chart}/FunnelChart.vue +80 -78
- package/{custom/widgets/chart/line → dist/custom/widgets/chart}/LineChart.vue +2 -2
- package/dist/custom/widgets/chart/{pie/PieChart.vue → PieChart.vue} +2 -2
- package/{custom/widgets/chart/stacked-bar → dist/custom/widgets/chart}/StackedBarChart.vue +97 -95
- package/dist/custom/widgets/chart/chart.types.d.ts +0 -2
- package/dist/custom/widgets/chart/chart.types.js +0 -23
- package/dist/custom/widgets/chart/chart.types.ts +0 -28
- package/dist/endpoint/dashboard.d.ts +6 -2
- package/dist/endpoint/dashboard.js +29 -5
- package/dist/endpoint/groups.d.ts +2 -21
- package/dist/endpoint/groups.js +18 -16
- package/dist/endpoint/widgets.d.ts +2 -4
- package/dist/endpoint/widgets.js +28 -74
- package/dist/index.js +1 -3
- package/dist/schema/api.d.ts +2172 -500
- package/dist/schema/api.js +21 -13
- package/dist/schema/widget.d.ts +1076 -263
- package/dist/schema/widget.js +108 -49
- package/dist/services/dashboardConfigService.d.ts +0 -10
- package/dist/services/dashboardConfigService.js +6 -21
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +266 -206
- package/endpoint/dashboard.ts +47 -7
- package/endpoint/groups.ts +25 -42
- package/endpoint/widgets.ts +41 -96
- package/index.ts +0 -3
- package/package.json +3 -3
- package/schema/api.ts +23 -13
- package/schema/widget.ts +119 -55
- package/services/dashboardConfigService.ts +6 -25
- package/services/widgetDataService.ts +350 -237
- package/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +0 -21
- package/dist/services/widgetConfigValidator.d.ts +0 -8
- package/dist/services/widgetConfigValidator.js +0 -27
- package/services/widgetConfigValidator.ts +0 -61
|
@@ -8,9 +8,9 @@ import type {
|
|
|
8
8
|
import type {
|
|
9
9
|
DashboardWidgetConfig,
|
|
10
10
|
DashboardWidgetData,
|
|
11
|
-
|
|
11
|
+
DashboardVariables,
|
|
12
12
|
FunnelQueryConfig,
|
|
13
|
-
|
|
13
|
+
FilterExpression,
|
|
14
14
|
QueryAggregateSelectItem,
|
|
15
15
|
QueryCalcSelectItem,
|
|
16
16
|
QueryConfig,
|
|
@@ -26,6 +26,7 @@ export type DashboardWidgetDataOptions = {
|
|
|
26
26
|
page: number;
|
|
27
27
|
pageSize: number;
|
|
28
28
|
};
|
|
29
|
+
variables?: DashboardVariables;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
type DashboardWidgetFilters =
|
|
@@ -33,14 +34,55 @@ type DashboardWidgetFilters =
|
|
|
33
34
|
| IAdminForthAndOrFilter
|
|
34
35
|
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
35
36
|
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
type AggregateRule =
|
|
38
|
+
| { operation: 'count' }
|
|
39
|
+
| { operation: Exclude<QueryAggregateSelectItem['agg'], 'count'>; field: string };
|
|
40
|
+
|
|
41
|
+
type AggregateGroupByRule =
|
|
42
|
+
| {
|
|
43
|
+
type: 'date_trunc';
|
|
44
|
+
field: string;
|
|
45
|
+
truncation: TimeGrain;
|
|
46
|
+
timezone?: string;
|
|
47
|
+
as: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: 'field';
|
|
51
|
+
field: string;
|
|
52
|
+
as: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type AggregateResource = {
|
|
56
|
+
aggregate: (
|
|
57
|
+
filters: DashboardWidgetFilters,
|
|
58
|
+
aggregations: Record<string, AggregateRule>,
|
|
59
|
+
groupBy?: AggregateGroupByRule | AggregateGroupByRule[],
|
|
60
|
+
) => Promise<Record<string, unknown>[]>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type EffectiveGroupByItem = {
|
|
64
|
+
field: string;
|
|
65
|
+
as: string;
|
|
66
|
+
grain?: TimeGrain;
|
|
67
|
+
timezone?: string;
|
|
39
68
|
};
|
|
40
69
|
|
|
41
|
-
const NOW_MINUS_RE = /^(\d+)([dhw])$/;
|
|
42
70
|
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
71
|
+
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;
|
|
72
|
+
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
43
73
|
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
74
|
+
const FILTER_OPERATORS = {
|
|
75
|
+
eq: Filters.EQ,
|
|
76
|
+
neq: Filters.NEQ,
|
|
77
|
+
gt: Filters.GT,
|
|
78
|
+
gte: Filters.GTE,
|
|
79
|
+
lt: Filters.LT,
|
|
80
|
+
lte: Filters.LTE,
|
|
81
|
+
in: Filters.IN,
|
|
82
|
+
not_in: Filters.NOT_IN,
|
|
83
|
+
like: Filters.LIKE,
|
|
84
|
+
ilike: Filters.ILIKE,
|
|
85
|
+
} as const;
|
|
44
86
|
|
|
45
87
|
export type WidgetDataService = {
|
|
46
88
|
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
@@ -56,8 +98,8 @@ export async function getWidgetData(
|
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
const data = 'steps' in widget.query
|
|
59
|
-
? await getFunnelWidgetData(adminforth, widget.query)
|
|
60
|
-
: await getQueryWidgetData(adminforth, widget.query);
|
|
101
|
+
? await getFunnelWidgetData(adminforth, widget.query, options.variables ?? {})
|
|
102
|
+
: await getQueryWidgetData(adminforth, widget.query, options.variables ?? {});
|
|
61
103
|
|
|
62
104
|
if (widget.target !== 'table' || !options.pagination) {
|
|
63
105
|
return data;
|
|
@@ -82,20 +124,38 @@ export async function getWidgetData(
|
|
|
82
124
|
async function getFunnelWidgetData(
|
|
83
125
|
adminforth: IAdminForth,
|
|
84
126
|
query: FunnelQueryConfig,
|
|
127
|
+
variables: DashboardVariables,
|
|
85
128
|
): Promise<DashboardWidgetData> {
|
|
86
129
|
const rows = await Promise.all(query.steps.map(async (step) => {
|
|
87
130
|
const valueField = step.metric.as;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
131
|
+
const [values = {}] = await getAggregateRows(
|
|
132
|
+
adminforth,
|
|
133
|
+
step.resource,
|
|
134
|
+
step.filters,
|
|
135
|
+
[step.metric],
|
|
136
|
+
[],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const row: Record<string, unknown> = {
|
|
91
140
|
name: step.name,
|
|
92
|
-
|
|
141
|
+
resource: step.resource,
|
|
142
|
+
[valueField]: values[valueField] ?? 0,
|
|
93
143
|
};
|
|
144
|
+
|
|
145
|
+
for (const calc of query.calcs ?? []) {
|
|
146
|
+
row[calc.as] = evaluateCalc(calc.calc, row, variables);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return row;
|
|
94
150
|
}));
|
|
95
151
|
|
|
96
152
|
return {
|
|
97
153
|
kind: 'aggregate',
|
|
98
|
-
columns: [
|
|
154
|
+
columns: [
|
|
155
|
+
'name',
|
|
156
|
+
...Array.from(new Set(query.steps.map((step) => step.metric.as))),
|
|
157
|
+
...Array.from(new Set((query.calcs ?? []).map((calc) => calc.as))),
|
|
158
|
+
],
|
|
99
159
|
rows,
|
|
100
160
|
};
|
|
101
161
|
}
|
|
@@ -103,10 +163,16 @@ async function getFunnelWidgetData(
|
|
|
103
163
|
async function getQueryWidgetData(
|
|
104
164
|
adminforth: IAdminForth,
|
|
105
165
|
query: QueryConfig,
|
|
166
|
+
variables: DashboardVariables,
|
|
106
167
|
): Promise<DashboardWidgetData> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
168
|
+
const selectedRows = isAggregateQuery(query)
|
|
169
|
+
? await buildAggregateQueryRows(adminforth, query, variables)
|
|
170
|
+
: buildPlainQueryRows(
|
|
171
|
+
await getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.order_by)),
|
|
172
|
+
query,
|
|
173
|
+
variables,
|
|
174
|
+
);
|
|
175
|
+
const orderedRows = sortRows(selectedRows, query.order_by);
|
|
110
176
|
const slicedRows = typeof query.limit === 'number'
|
|
111
177
|
? orderedRows.slice(query.offset ?? 0, (query.offset ?? 0) + query.limit)
|
|
112
178
|
: orderedRows.slice(query.offset ?? 0);
|
|
@@ -133,84 +199,99 @@ async function getQueryWidgetData(
|
|
|
133
199
|
async function getResourceRows(
|
|
134
200
|
adminforth: IAdminForth,
|
|
135
201
|
resourceId: string,
|
|
136
|
-
filters:
|
|
202
|
+
filters: FilterExpression | undefined,
|
|
137
203
|
sort?: IAdminForthSort | IAdminForthSort[],
|
|
138
204
|
) {
|
|
139
205
|
return adminforth.resource(resourceId).list(
|
|
140
|
-
|
|
206
|
+
getAdminForthFilters(filters),
|
|
141
207
|
undefined,
|
|
142
208
|
0,
|
|
143
209
|
sort,
|
|
144
210
|
);
|
|
145
211
|
}
|
|
146
212
|
|
|
147
|
-
function
|
|
213
|
+
function buildPlainQueryRows(rows: Record<string, unknown>[], query: QueryConfig, variables: DashboardVariables) {
|
|
148
214
|
const select = query.select ?? getDefaultSelect(rows);
|
|
149
|
-
|
|
215
|
+
return rows.map((row) => buildPlainRow(row, select, query.calcs, variables));
|
|
216
|
+
}
|
|
150
217
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
218
|
+
async function buildAggregateQueryRows(
|
|
219
|
+
adminforth: IAdminForth,
|
|
220
|
+
query: QueryConfig,
|
|
221
|
+
variables: DashboardVariables,
|
|
222
|
+
) {
|
|
223
|
+
const select = query.select ?? [];
|
|
224
|
+
const effectiveGroupBy = getEffectiveGroupBy(query.group_by, select);
|
|
225
|
+
const aggregateSelect = select.filter(isAggregateSelectItem);
|
|
226
|
+
const rows = await getAggregateRows(
|
|
227
|
+
adminforth,
|
|
228
|
+
query.resource,
|
|
229
|
+
query.filters,
|
|
230
|
+
aggregateSelect,
|
|
231
|
+
effectiveGroupBy,
|
|
232
|
+
);
|
|
154
233
|
|
|
155
|
-
return rows.map((row) =>
|
|
234
|
+
return rows.map((row) => buildCalculatedRow(row, select, query.calcs, variables));
|
|
156
235
|
}
|
|
157
236
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
237
|
+
async function getAggregateRows(
|
|
238
|
+
adminforth: IAdminForth,
|
|
239
|
+
resourceId: string,
|
|
240
|
+
baseFilters: FilterExpression | undefined,
|
|
241
|
+
select: QueryAggregateSelectItem[],
|
|
242
|
+
groupBy: EffectiveGroupByItem[],
|
|
163
243
|
) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
const
|
|
171
|
-
|
|
244
|
+
const resource = adminforth.resource(resourceId) as unknown as AggregateResource;
|
|
245
|
+
const groups = new Map<string, Record<string, unknown>>();
|
|
246
|
+
const groupByRules = groupBy.length ? groupBy.map(toAggregateGroupByRule) : undefined;
|
|
247
|
+
const aggregateSelectGroups = groupAggregateSelectItems(select);
|
|
248
|
+
|
|
249
|
+
if (groupBy.length) {
|
|
250
|
+
const groupSeedAlias = getHiddenAggregateAlias(groupBy, select);
|
|
251
|
+
const groupSeedRows = await resource.aggregate(
|
|
252
|
+
getAdminForthFilters(baseFilters),
|
|
253
|
+
{ [groupSeedAlias]: { operation: 'count' } },
|
|
254
|
+
groupByRules,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
for (const row of groupSeedRows) {
|
|
258
|
+
ensureAggregateGroup(groups, row, groupBy);
|
|
259
|
+
}
|
|
172
260
|
}
|
|
173
261
|
|
|
174
|
-
for (const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
262
|
+
for (const filterGroup of aggregateSelectGroups) {
|
|
263
|
+
const rows = await resource.aggregate(
|
|
264
|
+
mergeFilters(baseFilters, filterGroup.filters),
|
|
265
|
+
Object.fromEntries(filterGroup.items.map((item) => [item.as, toAggregationRule(item)])),
|
|
266
|
+
groupByRules,
|
|
267
|
+
);
|
|
179
268
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const key = JSON.stringify(values);
|
|
183
|
-
const group = groups.get(key) ?? { values, rows: [] };
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
const values = ensureAggregateGroup(groups, row, groupBy);
|
|
184
271
|
|
|
185
|
-
|
|
186
|
-
|
|
272
|
+
for (const item of filterGroup.items) {
|
|
273
|
+
values[item.as] = row[item.as] ?? 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
187
276
|
}
|
|
188
277
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
278
|
+
if (!groups.size && !groupBy.length && select.length) {
|
|
279
|
+
groups.set(JSON.stringify({}), {});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Array.from(groups.values(), (row) => applyAggregateDefaults(row, select));
|
|
193
283
|
}
|
|
194
284
|
|
|
195
|
-
function
|
|
196
|
-
|
|
285
|
+
function buildCalculatedRow(
|
|
286
|
+
baseValues: Record<string, unknown>,
|
|
197
287
|
select: QuerySelectItem[],
|
|
198
|
-
calcs: QueryCalcSelectItem[],
|
|
288
|
+
calcs: QueryCalcSelectItem[] = [],
|
|
289
|
+
variables: DashboardVariables,
|
|
199
290
|
) {
|
|
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
|
-
}
|
|
291
|
+
const values: Record<string, unknown> = { ...baseValues };
|
|
211
292
|
|
|
212
293
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
213
|
-
values[item.as] = evaluateCalc(item.calc, values);
|
|
294
|
+
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
214
295
|
}
|
|
215
296
|
|
|
216
297
|
return values;
|
|
@@ -220,6 +301,7 @@ function buildPlainRow(
|
|
|
220
301
|
row: Record<string, unknown>,
|
|
221
302
|
select: QuerySelectItem[],
|
|
222
303
|
calcs: QueryCalcSelectItem[] = [],
|
|
304
|
+
variables: DashboardVariables,
|
|
223
305
|
) {
|
|
224
306
|
const values: Record<string, unknown> = {};
|
|
225
307
|
|
|
@@ -232,58 +314,23 @@ function buildPlainRow(
|
|
|
232
314
|
}
|
|
233
315
|
|
|
234
316
|
for (const item of [...select.filter(isCalcSelectItem), ...calcs]) {
|
|
235
|
-
values[item.as] = evaluateCalc(item.calc, values);
|
|
317
|
+
values[item.as] = evaluateCalc(item.calc, values, variables);
|
|
236
318
|
}
|
|
237
319
|
|
|
238
320
|
return values;
|
|
239
321
|
}
|
|
240
322
|
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
}
|
|
323
|
+
function evaluateCalc(calc: string, values: Record<string, unknown>, variables: DashboardVariables) {
|
|
324
|
+
const expression = calc
|
|
325
|
+
.replace(LOOKUP_CALL_RE, (_match, path: string, keyField: string, defaultValue: string) => {
|
|
326
|
+
const map = resolveVariablePath(variables, path);
|
|
327
|
+
const key = String(values[keyField] ?? '');
|
|
263
328
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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])));
|
|
329
|
+
return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
|
|
330
|
+
? map[key]
|
|
331
|
+
: Number(defaultValue)));
|
|
332
|
+
})
|
|
333
|
+
.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
287
334
|
|
|
288
335
|
if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
|
|
289
336
|
throw new Error(`Unsupported calc expression: ${calc}`);
|
|
@@ -292,6 +339,14 @@ function evaluateCalc(calc: string, values: Record<string, unknown>) {
|
|
|
292
339
|
return Function(`"use strict"; return (${expression});`)();
|
|
293
340
|
}
|
|
294
341
|
|
|
342
|
+
function resolveVariablePath(variables: DashboardVariables, path: string) {
|
|
343
|
+
return path
|
|
344
|
+
.replace(VARIABLE_PATH_PREFIX_RE, '')
|
|
345
|
+
.split('.')
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
.reduce<unknown>((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
|
|
348
|
+
}
|
|
349
|
+
|
|
295
350
|
function sortRows(rows: Record<string, unknown>[], orderBy: QueryOrderByItem[] = []) {
|
|
296
351
|
if (!orderBy.length) {
|
|
297
352
|
return rows;
|
|
@@ -331,7 +386,7 @@ function getBackendSort(orderBy: QueryOrderByItem[] | undefined) {
|
|
|
331
386
|
|
|
332
387
|
function getColumns(rows: Record<string, unknown>[], query: QueryConfig) {
|
|
333
388
|
const selectColumns = [
|
|
334
|
-
...(query.
|
|
389
|
+
...(query.group_by ?? []).map(getGroupByAlias),
|
|
335
390
|
...(query.select ?? []).map(getSelectAlias),
|
|
336
391
|
...(query.calcs ?? []).map((item) => item.as),
|
|
337
392
|
].filter(Boolean);
|
|
@@ -345,7 +400,7 @@ function getDefaultSelect(rows: Record<string, unknown>[]): QuerySelectItem[] {
|
|
|
345
400
|
|
|
346
401
|
function isAggregateQuery(query: QueryConfig) {
|
|
347
402
|
return Boolean(
|
|
348
|
-
query.
|
|
403
|
+
query.group_by?.length
|
|
349
404
|
|| query.select?.some((item) => isAggregateSelectItem(item)),
|
|
350
405
|
);
|
|
351
406
|
}
|
|
@@ -370,206 +425,264 @@ function getSelectAlias(item: QuerySelectItem) {
|
|
|
370
425
|
return item.as;
|
|
371
426
|
}
|
|
372
427
|
|
|
373
|
-
function getGroupByField(item: QueryGroupByItem) {
|
|
374
|
-
return typeof item === 'string' ? item : item.field;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
428
|
function getGroupByAlias(item: QueryGroupByItem) {
|
|
378
429
|
return typeof item === 'string' ? item : item.as ?? item.field;
|
|
379
430
|
}
|
|
380
431
|
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
386
|
-
if (!grain) {
|
|
387
|
-
return value;
|
|
432
|
+
function getEffectiveGroupBy(groupBy: QueryGroupByItem[] | undefined, select: QuerySelectItem[]) {
|
|
433
|
+
if (groupBy?.length) {
|
|
434
|
+
return groupBy.map((item) => normalizeGroupByItem(item));
|
|
388
435
|
}
|
|
389
436
|
|
|
390
|
-
|
|
437
|
+
return select
|
|
438
|
+
.filter(isFieldSelectItem)
|
|
439
|
+
.map((item) => normalizeGroupByItem({ field: item.field, as: item.as, grain: item.grain }));
|
|
440
|
+
}
|
|
391
441
|
|
|
392
|
-
|
|
393
|
-
|
|
442
|
+
function normalizeGroupByItem(item: QueryGroupByItem | Pick<QueryFieldSelectItem, 'field' | 'as' | 'grain'>): EffectiveGroupByItem {
|
|
443
|
+
if (typeof item === 'string') {
|
|
444
|
+
return { field: item, as: item };
|
|
394
445
|
}
|
|
395
446
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
447
|
+
return {
|
|
448
|
+
field: item.field,
|
|
449
|
+
as: item.as ?? item.field,
|
|
450
|
+
grain: item.grain,
|
|
451
|
+
timezone: 'timezone' in item ? item.timezone : undefined,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
399
454
|
|
|
400
|
-
|
|
401
|
-
|
|
455
|
+
function toAggregateGroupByRule(item: EffectiveGroupByItem): AggregateGroupByRule {
|
|
456
|
+
if (item.grain) {
|
|
457
|
+
return {
|
|
458
|
+
type: 'date_trunc',
|
|
459
|
+
field: item.field,
|
|
460
|
+
truncation: item.grain,
|
|
461
|
+
timezone: item.timezone,
|
|
462
|
+
as: item.as,
|
|
463
|
+
};
|
|
402
464
|
}
|
|
403
465
|
|
|
404
|
-
|
|
466
|
+
return {
|
|
467
|
+
type: 'field',
|
|
468
|
+
field: item.field,
|
|
469
|
+
as: item.as,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
405
472
|
|
|
406
|
-
|
|
407
|
-
|
|
473
|
+
function toAggregationRule(item: QueryAggregateSelectItem): AggregateRule {
|
|
474
|
+
switch (item.agg) {
|
|
475
|
+
case 'count':
|
|
476
|
+
return { operation: 'count' };
|
|
477
|
+
case 'count_distinct':
|
|
478
|
+
return { operation: 'count_distinct', field: item.field! };
|
|
479
|
+
case 'sum':
|
|
480
|
+
return { operation: 'sum', field: item.field! };
|
|
481
|
+
case 'avg':
|
|
482
|
+
return { operation: 'avg', field: item.field! };
|
|
483
|
+
case 'min':
|
|
484
|
+
return { operation: 'min', field: item.field! };
|
|
485
|
+
case 'max':
|
|
486
|
+
return { operation: 'max', field: item.field! };
|
|
487
|
+
case 'median':
|
|
488
|
+
return { operation: 'median', field: item.field! };
|
|
408
489
|
}
|
|
490
|
+
}
|
|
409
491
|
|
|
410
|
-
|
|
492
|
+
function extractAggregateGroupValues(row: Record<string, unknown>, groupBy: EffectiveGroupByItem[]) {
|
|
493
|
+
return Object.fromEntries(groupBy.map((item) => [
|
|
494
|
+
item.as,
|
|
495
|
+
formatGroupValue(row[item.as], item.grain),
|
|
496
|
+
]));
|
|
497
|
+
}
|
|
411
498
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
499
|
+
function ensureAggregateGroup(
|
|
500
|
+
groups: Map<string, Record<string, unknown>>,
|
|
501
|
+
row: Record<string, unknown>,
|
|
502
|
+
groupBy: EffectiveGroupByItem[],
|
|
503
|
+
) {
|
|
504
|
+
const groupValues = groupBy.length ? extractAggregateGroupValues(row, groupBy) : {};
|
|
505
|
+
const key = JSON.stringify(groupValues);
|
|
506
|
+
const existingGroup = groups.get(key);
|
|
417
507
|
|
|
418
|
-
if (
|
|
419
|
-
return
|
|
508
|
+
if (existingGroup) {
|
|
509
|
+
return existingGroup;
|
|
420
510
|
}
|
|
421
511
|
|
|
422
|
-
|
|
423
|
-
return
|
|
512
|
+
groups.set(key, groupValues);
|
|
513
|
+
return groupValues;
|
|
424
514
|
}
|
|
425
515
|
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (isRecord(filters)) {
|
|
432
|
-
return normalizeFilterNode(filters);
|
|
516
|
+
function applyAggregateDefaults(values: Record<string, unknown>, select: QueryAggregateSelectItem[]) {
|
|
517
|
+
for (const item of select) {
|
|
518
|
+
if (typeof values[item.as] === 'undefined') {
|
|
519
|
+
values[item.as] = 0;
|
|
520
|
+
}
|
|
433
521
|
}
|
|
434
522
|
|
|
435
|
-
return
|
|
523
|
+
return values;
|
|
436
524
|
}
|
|
437
525
|
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
526
|
+
function groupAggregateSelectItems(select: QueryAggregateSelectItem[]) {
|
|
527
|
+
const groups = new Map<string, {
|
|
528
|
+
filters: DashboardWidgetFilters;
|
|
529
|
+
items: QueryAggregateSelectItem[];
|
|
530
|
+
}>();
|
|
442
531
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
532
|
+
for (const item of select) {
|
|
533
|
+
const filters = getAdminForthFilters(item.filters);
|
|
534
|
+
const key = getFilterCacheKey(filters);
|
|
535
|
+
const group = groups.get(key) ?? { filters, items: [] };
|
|
446
536
|
|
|
447
|
-
|
|
448
|
-
|
|
537
|
+
group.items.push(item);
|
|
538
|
+
groups.set(key, group);
|
|
449
539
|
}
|
|
450
540
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return Filters.EQ(filter.field, normalizeFilterValue(filter.eq));
|
|
454
|
-
}
|
|
541
|
+
return Array.from(groups.values());
|
|
542
|
+
}
|
|
455
543
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
544
|
+
function getFilterCacheKey(filters: DashboardWidgetFilters) {
|
|
545
|
+
if (Array.isArray(filters) && !filters.length) {
|
|
546
|
+
return '__base__';
|
|
547
|
+
}
|
|
459
548
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
549
|
+
return JSON.stringify(filters);
|
|
550
|
+
}
|
|
463
551
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
552
|
+
function mergeFilters(...filters: Array<FilterExpression | DashboardWidgetFilters | undefined>) {
|
|
553
|
+
const merged: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> = [];
|
|
467
554
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
555
|
+
for (const filter of filters) {
|
|
556
|
+
const normalized = getAdminForthFilters(filter);
|
|
471
557
|
|
|
472
|
-
if (
|
|
473
|
-
|
|
558
|
+
if (Array.isArray(normalized)) {
|
|
559
|
+
merged.push(...normalized);
|
|
560
|
+
continue;
|
|
474
561
|
}
|
|
475
562
|
|
|
476
|
-
if (
|
|
477
|
-
|
|
563
|
+
if (normalized) {
|
|
564
|
+
merged.push(normalized);
|
|
478
565
|
}
|
|
566
|
+
}
|
|
479
567
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
568
|
+
if (!merged.length) {
|
|
569
|
+
return [] as DashboardWidgetFilters;
|
|
483
570
|
}
|
|
484
571
|
|
|
485
|
-
return
|
|
572
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
486
573
|
}
|
|
487
574
|
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
575
|
+
function getHiddenAggregateAlias(groupBy: EffectiveGroupByItem[], select: QueryAggregateSelectItem[]) {
|
|
576
|
+
const usedAliases = new Set([
|
|
577
|
+
...groupBy.map((item) => item.as),
|
|
578
|
+
...select.map((item) => item.as),
|
|
579
|
+
]);
|
|
580
|
+
let alias = '__adminforth_dashboard_group_seed__';
|
|
492
581
|
|
|
493
|
-
|
|
494
|
-
|
|
582
|
+
while (usedAliases.has(alias)) {
|
|
583
|
+
alias = `_${alias}`;
|
|
495
584
|
}
|
|
496
585
|
|
|
497
|
-
|
|
498
|
-
|
|
586
|
+
return alias;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
590
|
+
if (!grain) {
|
|
591
|
+
return value;
|
|
499
592
|
}
|
|
500
593
|
|
|
501
|
-
const
|
|
594
|
+
const date = new Date(String(value));
|
|
502
595
|
|
|
503
|
-
if (
|
|
504
|
-
return value
|
|
596
|
+
if (!Number.isFinite(date.getTime())) {
|
|
597
|
+
return value;
|
|
505
598
|
}
|
|
506
599
|
|
|
507
|
-
if (
|
|
508
|
-
return
|
|
600
|
+
if (grain === 'year') {
|
|
601
|
+
return `${date.getUTCFullYear()}`;
|
|
509
602
|
}
|
|
510
603
|
|
|
511
|
-
|
|
512
|
-
|
|
604
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
605
|
+
|
|
606
|
+
if (grain === 'month') {
|
|
607
|
+
return `${date.getUTCFullYear()}-${month}`;
|
|
513
608
|
}
|
|
514
609
|
|
|
515
|
-
|
|
516
|
-
|
|
610
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
611
|
+
|
|
612
|
+
if (grain === 'week') {
|
|
613
|
+
const weekStart = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
614
|
+
weekStart.setUTCDate(weekStart.getUTCDate() - weekStart.getUTCDay());
|
|
615
|
+
return weekStart.toISOString().slice(0, 10);
|
|
517
616
|
}
|
|
518
617
|
|
|
519
|
-
if (
|
|
520
|
-
return
|
|
618
|
+
if (grain === 'day') {
|
|
619
|
+
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
521
620
|
}
|
|
522
621
|
|
|
523
|
-
|
|
524
|
-
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function getAdminForthFilters(filters: FilterExpression | DashboardWidgetFilters | undefined): DashboardWidgetFilters {
|
|
626
|
+
if (Array.isArray(filters)) {
|
|
627
|
+
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
628
|
+
? toAdminForthFilter(filter)
|
|
629
|
+
: filter);
|
|
525
630
|
}
|
|
526
631
|
|
|
527
|
-
if (
|
|
528
|
-
return
|
|
632
|
+
if (isDashboardFilterExpression(filters)) {
|
|
633
|
+
return toAdminForthFilter(filters);
|
|
529
634
|
}
|
|
530
635
|
|
|
531
|
-
if (
|
|
532
|
-
return
|
|
636
|
+
if (filters) {
|
|
637
|
+
return filters;
|
|
533
638
|
}
|
|
534
639
|
|
|
535
|
-
return
|
|
640
|
+
return [];
|
|
536
641
|
}
|
|
537
642
|
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
643
|
+
function isDashboardFilterExpression(value: unknown): value is FilterExpression {
|
|
644
|
+
if (Array.isArray(value)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
541
647
|
|
|
542
|
-
if (
|
|
543
|
-
return
|
|
648
|
+
if (!isRecord(value)) {
|
|
649
|
+
return false;
|
|
544
650
|
}
|
|
545
651
|
|
|
546
|
-
return
|
|
652
|
+
return 'and' in value
|
|
653
|
+
|| 'or' in value
|
|
654
|
+
|| 'eq' in value
|
|
655
|
+
|| 'neq' in value
|
|
656
|
+
|| 'gt' in value
|
|
657
|
+
|| 'gte' in value
|
|
658
|
+
|| 'lt' in value
|
|
659
|
+
|| 'lte' in value
|
|
660
|
+
|| 'in' in value
|
|
661
|
+
|| 'not_in' in value
|
|
662
|
+
|| 'like' in value
|
|
663
|
+
|| 'ilike' in value;
|
|
547
664
|
}
|
|
548
665
|
|
|
549
|
-
function
|
|
550
|
-
if (
|
|
551
|
-
return
|
|
666
|
+
function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
667
|
+
if (Array.isArray(filter)) {
|
|
668
|
+
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
552
669
|
}
|
|
553
670
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!match) {
|
|
557
|
-
return value;
|
|
671
|
+
if ('and' in filter) {
|
|
672
|
+
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
558
673
|
}
|
|
559
674
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
675
|
+
if ('or' in filter) {
|
|
676
|
+
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
677
|
+
}
|
|
563
678
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
} else {
|
|
569
|
-
date.setDate(date.getDate() - amount);
|
|
679
|
+
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
680
|
+
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
681
|
+
return createFilter(filter.field, filter[operator as keyof typeof FILTER_OPERATORS]);
|
|
682
|
+
}
|
|
570
683
|
}
|
|
571
684
|
|
|
572
|
-
return
|
|
685
|
+
return Filters.AND([]);
|
|
573
686
|
}
|
|
574
687
|
|
|
575
688
|
function toFiniteNumber(value: unknown) {
|