@adminforth/dashboard 1.8.0 → 1.10.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 +81 -55
- package/custom/api/dashboardApi.ts +73 -36
- package/custom/model/dashboard.types.ts +6 -13
- package/custom/runtime/DashboardRuntime.vue +26 -22
- package/custom/skills/adminforth-dashboard/SKILL.md +13 -20
- package/dist/custom/api/dashboardApi.d.ts +24 -18
- package/dist/custom/api/dashboardApi.js +42 -18
- package/dist/custom/api/dashboardApi.ts +73 -36
- package/dist/custom/model/dashboard.types.d.ts +0 -5
- package/dist/custom/model/dashboard.types.ts +6 -13
- package/dist/custom/queries/useDashboardConfig.d.ts +20 -120
- package/dist/custom/queries/useWidgetData.d.ts +20 -120
- package/dist/custom/runtime/DashboardRuntime.vue +26 -22
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +13 -20
- package/dist/endpoint/groups.js +22 -20
- package/dist/endpoint/widgets.js +28 -26
- package/dist/schema/api.d.ts +230 -3936
- package/dist/schema/api.js +7 -12
- package/dist/schema/widget.d.ts +20 -200
- package/dist/schema/widgets/charts.d.ts +24 -240
- package/dist/schema/widgets/common.d.ts +2 -20
- package/dist/schema/widgets/common.js +1 -10
- package/dist/schema/widgets/gauge-card.d.ts +2 -20
- package/dist/schema/widgets/kpi-card.d.ts +2 -20
- package/dist/schema/widgets/pivot-table.d.ts +2 -20
- package/dist/schema/widgets/table.d.ts +2 -20
- package/dist/services/calc-evaluator.d.ts +2 -0
- package/dist/services/calc-evaluator.js +54 -0
- package/dist/services/dashboardFilterService.d.ts +5 -0
- package/dist/services/dashboardFilterService.js +125 -0
- package/dist/services/widgetDataService.js +15 -168
- package/endpoint/groups.ts +22 -20
- package/endpoint/widgets.ts +28 -26
- package/package.json +2 -1
- package/schema/api.ts +7 -12
- package/schema/widgets/common.ts +1 -11
- package/services/calc-evaluator.ts +71 -0
- package/services/dashboardFilterService.ts +162 -0
- package/services/widgetDataService.ts +26 -213
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Sorts } from 'adminforth';
|
|
2
2
|
import type {
|
|
3
3
|
IAdminForth,
|
|
4
|
-
IAdminForthAndOrFilter,
|
|
5
|
-
IAdminForthSingleFilter,
|
|
6
4
|
IAdminForthSort,
|
|
7
5
|
} from 'adminforth';
|
|
8
6
|
import type {
|
|
@@ -21,6 +19,12 @@ import type {
|
|
|
21
19
|
QuerySelectItem,
|
|
22
20
|
TimeGrain,
|
|
23
21
|
} from '../custom/model/dashboard.types.js';
|
|
22
|
+
import {
|
|
23
|
+
getAdminForthFilters,
|
|
24
|
+
mergeFilters,
|
|
25
|
+
type DashboardQueryFilters,
|
|
26
|
+
} from './dashboardFilterService.js';
|
|
27
|
+
import { evaluateCalc } from './calc-evaluator.js';
|
|
24
28
|
|
|
25
29
|
export type DashboardWidgetDataOptions = {
|
|
26
30
|
pagination?: {
|
|
@@ -30,11 +34,6 @@ export type DashboardWidgetDataOptions = {
|
|
|
30
34
|
variables?: DashboardVariables;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
type DashboardWidgetFilters =
|
|
34
|
-
| IAdminForthSingleFilter
|
|
35
|
-
| IAdminForthAndOrFilter
|
|
36
|
-
| Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
|
|
37
|
-
|
|
38
37
|
type AggregateRule =
|
|
39
38
|
| { operation: 'count' }
|
|
40
39
|
| { operation: Exclude<QueryAggregateSelectItem['agg'], 'count'>; field: string };
|
|
@@ -55,7 +54,7 @@ type AggregateGroupByRule =
|
|
|
55
54
|
|
|
56
55
|
type AggregateResource = {
|
|
57
56
|
aggregate: (
|
|
58
|
-
filters:
|
|
57
|
+
filters: DashboardQueryFilters,
|
|
59
58
|
aggregations: Record<string, AggregateRule>,
|
|
60
59
|
groupBy?: AggregateGroupByRule | AggregateGroupByRule[],
|
|
61
60
|
) => Promise<Record<string, unknown>[]>;
|
|
@@ -68,24 +67,6 @@ type EffectiveGroupByItem = {
|
|
|
68
67
|
timezone?: string;
|
|
69
68
|
};
|
|
70
69
|
|
|
71
|
-
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
72
|
-
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;
|
|
73
|
-
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
74
|
-
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
|
|
75
|
-
const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
|
|
76
|
-
const FILTER_OPERATORS = {
|
|
77
|
-
eq: Filters.EQ,
|
|
78
|
-
neq: Filters.NEQ,
|
|
79
|
-
gt: Filters.GT,
|
|
80
|
-
gte: Filters.GTE,
|
|
81
|
-
lt: Filters.LT,
|
|
82
|
-
lte: Filters.LTE,
|
|
83
|
-
in: Filters.IN,
|
|
84
|
-
not_in: Filters.NOT_IN,
|
|
85
|
-
like: Filters.LIKE,
|
|
86
|
-
ilike: Filters.ILIKE,
|
|
87
|
-
} as const;
|
|
88
|
-
|
|
89
70
|
export type WidgetDataService = {
|
|
90
71
|
getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
|
|
91
72
|
};
|
|
@@ -130,10 +111,10 @@ async function getQueryWidgetData(
|
|
|
130
111
|
return getStepsQueryData(adminforth, query, variables);
|
|
131
112
|
}
|
|
132
113
|
|
|
133
|
-
const
|
|
114
|
+
const singleAggregateSelect = getSingleAggregateSelectItem(query);
|
|
134
115
|
|
|
135
|
-
if (
|
|
136
|
-
return
|
|
116
|
+
if (singleAggregateSelect) {
|
|
117
|
+
return getSingleAggregateWidgetData(adminforth, query, singleAggregateSelect);
|
|
137
118
|
}
|
|
138
119
|
|
|
139
120
|
const selectedRows = isAggregateQuery(query)
|
|
@@ -207,27 +188,27 @@ async function getStepsQueryData(
|
|
|
207
188
|
};
|
|
208
189
|
}
|
|
209
190
|
|
|
210
|
-
async function
|
|
191
|
+
async function getSingleAggregateWidgetData(
|
|
211
192
|
adminforth: IAdminForth,
|
|
212
193
|
query: ResourceQueryConfig,
|
|
213
|
-
|
|
194
|
+
aggregate: QueryAggregateSelectItem,
|
|
214
195
|
): Promise<DashboardWidgetData> {
|
|
215
196
|
const [currentValues = {}] = await getAggregateRows(
|
|
216
197
|
adminforth,
|
|
217
198
|
query.resource,
|
|
218
199
|
query.filters,
|
|
219
|
-
[
|
|
200
|
+
[aggregate],
|
|
220
201
|
[],
|
|
221
202
|
);
|
|
222
203
|
const values: Record<string, unknown> = {
|
|
223
|
-
[
|
|
204
|
+
[aggregate.as]: currentValues[aggregate.as] ?? 0,
|
|
224
205
|
};
|
|
225
206
|
|
|
226
207
|
const rows = query.sparkline
|
|
227
|
-
? await
|
|
208
|
+
? await getSingleAggregateSparklineRows(adminforth, query, aggregate, getAdminForthFilters(query.filters))
|
|
228
209
|
: [values];
|
|
229
210
|
const columns = Array.from(new Set([
|
|
230
|
-
|
|
211
|
+
aggregate.as,
|
|
231
212
|
...(query.sparkline ? [query.sparkline.as] : []),
|
|
232
213
|
]));
|
|
233
214
|
|
|
@@ -239,11 +220,11 @@ async function getMetricWidgetData(
|
|
|
239
220
|
};
|
|
240
221
|
}
|
|
241
222
|
|
|
242
|
-
async function
|
|
223
|
+
async function getSingleAggregateSparklineRows(
|
|
243
224
|
adminforth: IAdminForth,
|
|
244
225
|
query: ResourceQueryConfig,
|
|
245
|
-
|
|
246
|
-
filters:
|
|
226
|
+
aggregate: QueryAggregateSelectItem,
|
|
227
|
+
filters: DashboardQueryFilters,
|
|
247
228
|
) {
|
|
248
229
|
const sparkline = query.sparkline!;
|
|
249
230
|
const groupBy = [{
|
|
@@ -255,7 +236,7 @@ async function getMetricSparklineRows(
|
|
|
255
236
|
adminforth,
|
|
256
237
|
query.resource,
|
|
257
238
|
filters,
|
|
258
|
-
[
|
|
239
|
+
[aggregate],
|
|
259
240
|
groupBy,
|
|
260
241
|
);
|
|
261
242
|
|
|
@@ -306,7 +287,7 @@ async function buildAggregateQueryRows(
|
|
|
306
287
|
async function getAggregateRows(
|
|
307
288
|
adminforth: IAdminForth,
|
|
308
289
|
resourceId: string,
|
|
309
|
-
baseFilters: FilterExpression |
|
|
290
|
+
baseFilters: FilterExpression | DashboardQueryFilters | undefined,
|
|
310
291
|
select: QueryAggregateSelectItem[],
|
|
311
292
|
groupBy: EffectiveGroupByItem[],
|
|
312
293
|
) {
|
|
@@ -389,33 +370,6 @@ function buildPlainRow(
|
|
|
389
370
|
return values;
|
|
390
371
|
}
|
|
391
372
|
|
|
392
|
-
function evaluateCalc(calc: string, values: Record<string, unknown>, variables: DashboardVariables) {
|
|
393
|
-
const expression = calc
|
|
394
|
-
.replace(LOOKUP_CALL_RE, (_match, path: string, keyField: string, defaultValue: string) => {
|
|
395
|
-
const map = resolveVariablePath(variables, path);
|
|
396
|
-
const key = String(values[keyField] ?? '');
|
|
397
|
-
|
|
398
|
-
return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
|
|
399
|
-
? map[key]
|
|
400
|
-
: Number(defaultValue)));
|
|
401
|
-
})
|
|
402
|
-
.replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
|
|
403
|
-
|
|
404
|
-
if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
|
|
405
|
-
throw new Error(`Unsupported calc expression: ${calc}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return Function(`"use strict"; return (${expression});`)();
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function resolveVariablePath(variables: DashboardVariables, path: string) {
|
|
412
|
-
return path
|
|
413
|
-
.replace(VARIABLE_PATH_PREFIX_RE, '')
|
|
414
|
-
.split('.')
|
|
415
|
-
.filter(Boolean)
|
|
416
|
-
.reduce<unknown>((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
373
|
function sortRows(rows: Record<string, unknown>[], orderBy: QueryOrderByItem[] = []) {
|
|
420
374
|
if (!orderBy.length) {
|
|
421
375
|
return rows;
|
|
@@ -474,7 +428,7 @@ function isAggregateQuery(query: ResourceQueryConfig) {
|
|
|
474
428
|
);
|
|
475
429
|
}
|
|
476
430
|
|
|
477
|
-
function
|
|
431
|
+
function getSingleAggregateSelectItem(query: ResourceQueryConfig) {
|
|
478
432
|
if (query.group_by?.length) {
|
|
479
433
|
return undefined;
|
|
480
434
|
}
|
|
@@ -494,7 +448,7 @@ function isStepsQuery(query: QueryConfig): query is Extract<QueryConfig, { sourc
|
|
|
494
448
|
}
|
|
495
449
|
|
|
496
450
|
function getStepSelect(step: StepsQueryStepConfig): QueryAggregateSelectItem[] {
|
|
497
|
-
return
|
|
451
|
+
return step.select;
|
|
498
452
|
}
|
|
499
453
|
|
|
500
454
|
function isFieldSelectItem(item: QuerySelectItem): item is QueryFieldSelectItem {
|
|
@@ -617,7 +571,7 @@ function applyAggregateDefaults(values: Record<string, unknown>, select: QueryAg
|
|
|
617
571
|
|
|
618
572
|
function groupAggregateSelectItems(select: QueryAggregateSelectItem[]) {
|
|
619
573
|
const groups = new Map<string, {
|
|
620
|
-
filters:
|
|
574
|
+
filters: DashboardQueryFilters;
|
|
621
575
|
items: QueryAggregateSelectItem[];
|
|
622
576
|
}>();
|
|
623
577
|
|
|
@@ -633,7 +587,7 @@ function groupAggregateSelectItems(select: QueryAggregateSelectItem[]) {
|
|
|
633
587
|
return Array.from(groups.values());
|
|
634
588
|
}
|
|
635
589
|
|
|
636
|
-
function getFilterCacheKey(filters:
|
|
590
|
+
function getFilterCacheKey(filters: DashboardQueryFilters) {
|
|
637
591
|
if (Array.isArray(filters) && !filters.length) {
|
|
638
592
|
return '__base__';
|
|
639
593
|
}
|
|
@@ -641,29 +595,6 @@ function getFilterCacheKey(filters: DashboardWidgetFilters) {
|
|
|
641
595
|
return JSON.stringify(filters);
|
|
642
596
|
}
|
|
643
597
|
|
|
644
|
-
function mergeFilters(...filters: Array<FilterExpression | DashboardWidgetFilters | undefined>) {
|
|
645
|
-
const merged: Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> = [];
|
|
646
|
-
|
|
647
|
-
for (const filter of filters) {
|
|
648
|
-
const normalized = getAdminForthFilters(filter);
|
|
649
|
-
|
|
650
|
-
if (Array.isArray(normalized)) {
|
|
651
|
-
merged.push(...normalized);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (normalized) {
|
|
656
|
-
merged.push(normalized);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (!merged.length) {
|
|
661
|
-
return [] as DashboardWidgetFilters;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return merged.length === 1 ? merged[0] : merged;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
598
|
function getHiddenAggregateAlias(groupBy: EffectiveGroupByItem[], select: QueryAggregateSelectItem[]) {
|
|
668
599
|
const usedAliases = new Set([
|
|
669
600
|
...groupBy.map((item) => item.as),
|
|
@@ -714,124 +645,6 @@ function formatGroupValue(value: unknown, grain: TimeGrain | undefined) {
|
|
|
714
645
|
return value;
|
|
715
646
|
}
|
|
716
647
|
|
|
717
|
-
function getAdminForthFilters(filters: FilterExpression | DashboardWidgetFilters | undefined): DashboardWidgetFilters {
|
|
718
|
-
if (Array.isArray(filters)) {
|
|
719
|
-
return filters.map((filter) => isDashboardFilterExpression(filter)
|
|
720
|
-
? toAdminForthFilter(filter)
|
|
721
|
-
: filter);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (isDashboardFilterExpression(filters)) {
|
|
725
|
-
return toAdminForthFilter(filters);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (filters) {
|
|
729
|
-
return filters;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return [];
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function isDashboardFilterExpression(value: unknown): value is FilterExpression {
|
|
736
|
-
if (Array.isArray(value)) {
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (!isRecord(value)) {
|
|
741
|
-
return false;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return 'and' in value
|
|
745
|
-
|| 'or' in value
|
|
746
|
-
|| 'eq' in value
|
|
747
|
-
|| 'neq' in value
|
|
748
|
-
|| 'gt' in value
|
|
749
|
-
|| 'gte' in value
|
|
750
|
-
|| 'lt' in value
|
|
751
|
-
|| 'lte' in value
|
|
752
|
-
|| 'in' in value
|
|
753
|
-
|| 'not_in' in value
|
|
754
|
-
|| 'like' in value
|
|
755
|
-
|| 'ilike' in value;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function toAdminForthFilter(filter: FilterExpression): IAdminForthSingleFilter | IAdminForthAndOrFilter {
|
|
759
|
-
if (Array.isArray(filter)) {
|
|
760
|
-
return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if ('and' in filter) {
|
|
764
|
-
return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if ('or' in filter) {
|
|
768
|
-
return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
772
|
-
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
773
|
-
return createFilter(filter.field, resolveFilterValue(filter[operator as keyof typeof FILTER_OPERATORS]));
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return Filters.AND([]);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function resolveFilterValue(value: unknown): unknown {
|
|
781
|
-
if (Array.isArray(value)) {
|
|
782
|
-
return value.map((item) => resolveFilterValue(item));
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (!isRecord(value)) {
|
|
786
|
-
return value;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (value.now === true) {
|
|
790
|
-
return new Date().toISOString();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (typeof value.now_minus === 'string') {
|
|
794
|
-
return subtractDuration(new Date(), value.now_minus).toISOString();
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return value;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function subtractDuration(now: Date, duration: string) {
|
|
801
|
-
const match = duration.match(RELATIVE_DURATION_RE);
|
|
802
|
-
|
|
803
|
-
if (!match) {
|
|
804
|
-
throw new Error(`Unsupported relative date duration: ${duration}`);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const amount = Number(match[1]);
|
|
808
|
-
const unit = match[2];
|
|
809
|
-
const date = new Date(now);
|
|
810
|
-
|
|
811
|
-
if (unit === 'h') {
|
|
812
|
-
date.setUTCHours(date.getUTCHours() - amount);
|
|
813
|
-
} else if (unit === 'd') {
|
|
814
|
-
date.setUTCDate(date.getUTCDate() - amount);
|
|
815
|
-
} else if (unit === 'w') {
|
|
816
|
-
date.setUTCDate(date.getUTCDate() - amount * 7);
|
|
817
|
-
} else if (unit === 'mo') {
|
|
818
|
-
date.setUTCMonth(date.getUTCMonth() - amount);
|
|
819
|
-
} else if (unit === 'y') {
|
|
820
|
-
date.setUTCFullYear(date.getUTCFullYear() - amount);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return date;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function toFiniteNumber(value: unknown) {
|
|
827
|
-
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
828
|
-
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function isRecord(value: unknown): value is Record<string, any> {
|
|
832
|
-
return typeof value === 'object' && value !== null;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
648
|
export function createWidgetDataService(adminforth: IAdminForth): WidgetDataService {
|
|
836
649
|
return {
|
|
837
650
|
getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
|