@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.
Files changed (39) hide show
  1. package/README.md +81 -55
  2. package/custom/api/dashboardApi.ts +73 -36
  3. package/custom/model/dashboard.types.ts +6 -13
  4. package/custom/runtime/DashboardRuntime.vue +26 -22
  5. package/custom/skills/adminforth-dashboard/SKILL.md +13 -20
  6. package/dist/custom/api/dashboardApi.d.ts +24 -18
  7. package/dist/custom/api/dashboardApi.js +42 -18
  8. package/dist/custom/api/dashboardApi.ts +73 -36
  9. package/dist/custom/model/dashboard.types.d.ts +0 -5
  10. package/dist/custom/model/dashboard.types.ts +6 -13
  11. package/dist/custom/queries/useDashboardConfig.d.ts +20 -120
  12. package/dist/custom/queries/useWidgetData.d.ts +20 -120
  13. package/dist/custom/runtime/DashboardRuntime.vue +26 -22
  14. package/dist/custom/skills/adminforth-dashboard/SKILL.md +13 -20
  15. package/dist/endpoint/groups.js +22 -20
  16. package/dist/endpoint/widgets.js +28 -26
  17. package/dist/schema/api.d.ts +230 -3936
  18. package/dist/schema/api.js +7 -12
  19. package/dist/schema/widget.d.ts +20 -200
  20. package/dist/schema/widgets/charts.d.ts +24 -240
  21. package/dist/schema/widgets/common.d.ts +2 -20
  22. package/dist/schema/widgets/common.js +1 -10
  23. package/dist/schema/widgets/gauge-card.d.ts +2 -20
  24. package/dist/schema/widgets/kpi-card.d.ts +2 -20
  25. package/dist/schema/widgets/pivot-table.d.ts +2 -20
  26. package/dist/schema/widgets/table.d.ts +2 -20
  27. package/dist/services/calc-evaluator.d.ts +2 -0
  28. package/dist/services/calc-evaluator.js +54 -0
  29. package/dist/services/dashboardFilterService.d.ts +5 -0
  30. package/dist/services/dashboardFilterService.js +125 -0
  31. package/dist/services/widgetDataService.js +15 -168
  32. package/endpoint/groups.ts +22 -20
  33. package/endpoint/widgets.ts +28 -26
  34. package/package.json +2 -1
  35. package/schema/api.ts +7 -12
  36. package/schema/widgets/common.ts +1 -11
  37. package/services/calc-evaluator.ts +71 -0
  38. package/services/dashboardFilterService.ts +162 -0
  39. package/services/widgetDataService.ts +26 -213
@@ -1,8 +1,6 @@
1
- import { Filters, Sorts } from 'adminforth';
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: DashboardWidgetFilters,
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 metricSelect = getSingleAggregateMetricSelect(query);
114
+ const singleAggregateSelect = getSingleAggregateSelectItem(query);
134
115
 
135
- if (metricSelect) {
136
- return getMetricWidgetData(adminforth, query, metricSelect);
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 getMetricWidgetData(
191
+ async function getSingleAggregateWidgetData(
211
192
  adminforth: IAdminForth,
212
193
  query: ResourceQueryConfig,
213
- metric: QueryAggregateSelectItem,
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
- [metric],
200
+ [aggregate],
220
201
  [],
221
202
  );
222
203
  const values: Record<string, unknown> = {
223
- [metric.as]: currentValues[metric.as] ?? 0,
204
+ [aggregate.as]: currentValues[aggregate.as] ?? 0,
224
205
  };
225
206
 
226
207
  const rows = query.sparkline
227
- ? await getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
208
+ ? await getSingleAggregateSparklineRows(adminforth, query, aggregate, getAdminForthFilters(query.filters))
228
209
  : [values];
229
210
  const columns = Array.from(new Set([
230
- metric.as,
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 getMetricSparklineRows(
223
+ async function getSingleAggregateSparklineRows(
243
224
  adminforth: IAdminForth,
244
225
  query: ResourceQueryConfig,
245
- metric: QueryAggregateSelectItem,
246
- filters: DashboardWidgetFilters,
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
- [metric],
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 | DashboardWidgetFilters | undefined,
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 getSingleAggregateMetricSelect(query: ResourceQueryConfig) {
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 'select' in step ? step.select : [step.metric];
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: DashboardWidgetFilters;
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: DashboardWidgetFilters) {
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),