@adminforth/dashboard 1.0.0 → 1.2.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 (55) hide show
  1. package/README.md +99 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +353 -2
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/DashboardRuntime.vue +2 -1
  6. package/custom/runtime/WidgetRenderer.vue +2 -1
  7. package/custom/runtime/WidgetShell.vue +8 -4
  8. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  9. package/custom/widgets/chart/ChartWidget.vue +45 -12
  10. package/custom/widgets/chart/chart.types.ts +83 -0
  11. package/custom/widgets/chart/chart.utils.ts +2 -2
  12. package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  13. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  14. package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  15. package/custom/widgets/table/TableWidget.vue +155 -30
  16. package/dist/custom/api/dashboardApi.d.ts +7 -1
  17. package/dist/custom/api/dashboardApi.js +4 -6
  18. package/dist/custom/api/dashboardApi.ts +9 -0
  19. package/dist/custom/model/dashboard.types.d.ts +70 -1
  20. package/dist/custom/model/dashboard.types.js +173 -1
  21. package/dist/custom/model/dashboard.types.ts +353 -2
  22. package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
  23. package/dist/custom/queries/useWidgetData.d.ts +44 -3
  24. package/dist/custom/queries/useWidgetData.js +3 -3
  25. package/dist/custom/queries/useWidgetData.ts +8 -4
  26. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  27. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  28. package/dist/custom/runtime/WidgetShell.vue +8 -4
  29. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  30. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  31. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  32. package/dist/custom/widgets/chart/chart.types.js +46 -0
  33. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  34. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  35. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  36. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  37. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  38. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  39. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  40. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  41. package/dist/endpoint/widgets.d.ts +6 -1
  42. package/dist/endpoint/widgets.js +41 -6
  43. package/dist/schema/api.d.ts +874 -444
  44. package/dist/schema/api.js +11 -2
  45. package/dist/schema/widget.d.ts +538 -132
  46. package/dist/schema/widget.js +138 -14
  47. package/dist/services/widgetConfigValidator.js +26 -40
  48. package/dist/services/widgetDataService.d.ts +7 -14
  49. package/dist/services/widgetDataService.js +115 -11
  50. package/endpoint/widgets.ts +56 -6
  51. package/package.json +1 -1
  52. package/schema/api.ts +11 -1
  53. package/schema/widget.ts +145 -15
  54. package/services/widgetConfigValidator.ts +36 -44
  55. package/services/widgetDataService.ts +175 -28
package/schema/widget.ts CHANGED
@@ -13,6 +13,62 @@ const DashboardWidgetSizeSchema = z.enum([
13
13
  'full',
14
14
  ])
15
15
 
16
+ export const AggregationOperationZodSchema = z.enum([
17
+ 'sum',
18
+ 'count',
19
+ 'avg',
20
+ 'min',
21
+ 'max',
22
+ 'median',
23
+ ])
24
+
25
+ export const AggregationRuleZodSchema = z.object({
26
+ operation: AggregationOperationZodSchema,
27
+ field: z.string().optional(),
28
+ }).superRefine((rule, ctx) => {
29
+ if (rule.operation !== 'count' && !rule.field) {
30
+ ctx.addIssue({
31
+ code: z.ZodIssueCode.custom,
32
+ path: ['field'],
33
+ message: `field is required for ${rule.operation}`,
34
+ })
35
+ }
36
+ })
37
+
38
+ export const GroupByRuleZodSchema = z.discriminatedUnion('type', [
39
+ z.object({
40
+ type: z.literal('field'),
41
+ field: z.string(),
42
+ }),
43
+ z.object({
44
+ type: z.literal('date_trunc'),
45
+ field: z.string(),
46
+ truncation: z.enum(['day', 'week', 'month', 'year']),
47
+ timezone: z.string().optional(),
48
+ }),
49
+ ])
50
+
51
+ export const AggregateDataSourceZodSchema = z.object({
52
+ type: z.literal('aggregate'),
53
+ resourceId: z.string(),
54
+ aggregations: z.record(z.string(), AggregationRuleZodSchema),
55
+ groupBy: GroupByRuleZodSchema.optional(),
56
+ filters: z.unknown().optional(),
57
+ }).strict()
58
+
59
+ export const ResourceDataSourceZodSchema = z.object({
60
+ type: z.literal('resource'),
61
+ resourceId: z.string(),
62
+ columns: z.array(z.string()).optional(),
63
+ filters: z.unknown().optional(),
64
+ sort: z.unknown().optional(),
65
+ }).strict()
66
+
67
+ export const WidgetDataSourceZodSchema = z.discriminatedUnion('type', [
68
+ ResourceDataSourceZodSchema,
69
+ AggregateDataSourceZodSchema,
70
+ ])
71
+
16
72
  const WidgetBaseSchema = z.object({
17
73
  id: z.string().optional(),
18
74
  group_id: z.string().optional(),
@@ -23,6 +79,7 @@ const WidgetBaseSchema = z.object({
23
79
  minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
24
80
  maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
25
81
  order: z.number().optional(),
82
+ dataSource: WidgetDataSourceZodSchema.optional(),
26
83
  })
27
84
 
28
85
  const ChartBaseSchema = z.object({
@@ -97,16 +154,6 @@ export const ChartConfigSchema = z.discriminatedUnion('type', [
97
154
  FunnelChartSchema,
98
155
  ])
99
156
 
100
- export const DashboardWidgetQuerySchema = z.object({
101
- resource: z.string().min(1, 'Query resource must be a non-empty string'),
102
- select: z.array(z.string()).optional(),
103
- order: z.object({
104
- field: z.string().min(1, 'Order field is required'),
105
- direction: z.enum(['asc', 'desc']),
106
- }).optional(),
107
- limit: z.number().optional(),
108
- })
109
-
110
157
  export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
111
158
  target: z.literal('empty'),
112
159
  })
@@ -114,31 +161,114 @@ export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
114
161
  const TableWidgetConfigSchema = WidgetBaseSchema.extend({
115
162
  target: z.literal('table'),
116
163
  table: z.unknown().optional(),
117
- query: DashboardWidgetQuerySchema.optional(),
164
+ }).superRefine((widget, ctx) => {
165
+ if (!widget.dataSource) {
166
+ ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ path: ['dataSource'],
169
+ message: 'Table widget must have dataSource config',
170
+ })
171
+ }
172
+
173
+ if (widget.dataSource?.type === 'aggregate') {
174
+ ctx.addIssue({
175
+ code: z.ZodIssueCode.custom,
176
+ path: ['dataSource'],
177
+ message: 'Table widget dataSource must use resource type',
178
+ })
179
+ }
118
180
  })
119
181
 
120
182
  const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
121
183
  target: z.literal('chart'),
122
184
  chart: ChartConfigSchema,
123
- query: DashboardWidgetQuerySchema,
185
+ }).superRefine((widget, ctx) => {
186
+ if (!widget.dataSource) {
187
+ ctx.addIssue({
188
+ code: z.ZodIssueCode.custom,
189
+ path: ['dataSource'],
190
+ message: 'Chart widget must have dataSource config',
191
+ })
192
+ }
193
+
194
+ if (widget.dataSource?.type === 'resource') {
195
+ ctx.addIssue({
196
+ code: z.ZodIssueCode.custom,
197
+ path: ['dataSource'],
198
+ message: 'Chart widget dataSource must use aggregate type',
199
+ })
200
+ }
201
+
202
+ if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
203
+ ctx.addIssue({
204
+ code: z.ZodIssueCode.custom,
205
+ path: ['dataSource', 'groupBy'],
206
+ message: 'Chart widget aggregate dataSource must define groupBy',
207
+ })
208
+ }
124
209
  })
125
210
 
126
211
  const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
127
212
  target: z.literal('kpi_card'),
128
213
  kpi_card: z.unknown().optional(),
129
- query: DashboardWidgetQuerySchema.optional(),
214
+ }).superRefine((widget, ctx) => {
215
+ if (!widget.dataSource) {
216
+ ctx.addIssue({
217
+ code: z.ZodIssueCode.custom,
218
+ path: ['dataSource'],
219
+ message: 'KPI card widget must have dataSource config',
220
+ })
221
+ }
222
+
223
+ if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
224
+ ctx.addIssue({
225
+ code: z.ZodIssueCode.custom,
226
+ path: ['dataSource', 'groupBy'],
227
+ message: 'KPI card aggregate dataSource must not define groupBy',
228
+ })
229
+ }
130
230
  })
131
231
 
132
232
  const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
133
233
  target: z.literal('gauge_card'),
134
234
  gauge_card: z.unknown().optional(),
135
- query: DashboardWidgetQuerySchema.optional(),
235
+ }).superRefine((widget, ctx) => {
236
+ if (!widget.dataSource) {
237
+ ctx.addIssue({
238
+ code: z.ZodIssueCode.custom,
239
+ path: ['dataSource'],
240
+ message: 'Gauge card widget must have dataSource config',
241
+ })
242
+ }
243
+
244
+ if (widget.dataSource?.type === 'aggregate' && widget.dataSource.groupBy) {
245
+ ctx.addIssue({
246
+ code: z.ZodIssueCode.custom,
247
+ path: ['dataSource', 'groupBy'],
248
+ message: 'Gauge card aggregate dataSource must not define groupBy',
249
+ })
250
+ }
136
251
  })
137
252
 
138
253
  const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
139
254
  target: z.literal('pivot_table'),
140
255
  pivot_table: z.unknown().optional(),
141
- query: DashboardWidgetQuerySchema.optional(),
256
+ }).superRefine((widget, ctx) => {
257
+ if (!widget.dataSource) {
258
+ ctx.addIssue({
259
+ code: z.ZodIssueCode.custom,
260
+ path: ['dataSource'],
261
+ message: 'Pivot table widget must have dataSource config',
262
+ })
263
+ }
264
+
265
+ if (widget.dataSource?.type === 'aggregate' && !widget.dataSource.groupBy) {
266
+ ctx.addIssue({
267
+ code: z.ZodIssueCode.custom,
268
+ path: ['dataSource', 'groupBy'],
269
+ message: 'Pivot table aggregate dataSource must define groupBy',
270
+ })
271
+ }
142
272
  })
143
273
 
144
274
  export const WidgetConfigSchema = z.discriminatedUnion('target', [
@@ -1,7 +1,7 @@
1
1
  import type { IAdminForth } from 'adminforth';
2
2
  import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
+ import { normalizeChartWidgetConfig } from '../custom/widgets/chart/chart.types.js';
3
4
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
- import type { DashboardWidgetQueryConfig } from './widgetDataService.js';
5
5
 
6
6
  export type WidgetConfigValidatorService = {
7
7
  validateDashboardWidgetApiConfig: (
@@ -19,15 +19,9 @@ export function validateDashboardWidgetApiConfig(
19
19
 
20
20
  const errors: DashboardWidgetConfigValidationError[] = [];
21
21
 
22
- if (!widget.query) {
23
- errors.push({
24
- field: 'query',
25
- message: 'Chart widget must have query config',
26
- });
27
- return errors;
28
- }
22
+ const chart = normalizeChartWidgetConfig(widget.chart);
29
23
 
30
- if (!widget.chart) {
24
+ if (!chart) {
31
25
  errors.push({
32
26
  field: 'chart',
33
27
  message: 'Chart widget must have chart config',
@@ -35,52 +29,33 @@ export function validateDashboardWidgetApiConfig(
35
29
  return errors;
36
30
  }
37
31
 
38
- const query = widget.query as DashboardWidgetQueryConfig;
39
- const chart = widget.chart;
40
-
41
- const resource = adminforth.config.resources.find((item) => item.resourceId === query.resource);
42
-
43
- if (!resource) {
44
- errors.push({
45
- field: 'query.resource',
46
- message: `Resource "${query.resource}" is not registered`,
47
- });
48
- return errors;
49
- }
32
+ const aggregateDataSource = getAggregateDataSource(widget.dataSource);
50
33
 
51
- if (!query.select) {
52
- return errors;
53
- }
34
+ if (aggregateDataSource) {
35
+ const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
54
36
 
55
- const resourceFields = resource.columns.map((column) => column.name);
56
-
57
- for (const field of query.select) {
58
- if (!resourceFields.includes(field)) {
37
+ if (!resource) {
59
38
  errors.push({
60
- field: 'query.select',
61
- message: `Field "${field}" is not in resource "${query.resource}"`,
39
+ field: 'data_source.resource_id',
40
+ message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
62
41
  });
63
42
  }
64
- }
65
-
66
- const chartFields = [
67
- chart.x_field,
68
- chart.y_field,
69
- chart.label_field,
70
- chart.value_field,
71
- chart.bucket_field,
72
- ...(chart.series?.map((series: { field: string }) => series.field) ?? []),
73
- ].filter((field): field is string => typeof field === 'string');
74
43
 
75
- for (const field of chartFields) {
76
- if (!query.select.includes(field)) {
44
+ if (!aggregateDataSource.groupBy) {
77
45
  errors.push({
78
- field: 'query.select',
79
- message: `Query select must include chart field "${field}"`,
46
+ field: 'data_source.group_by',
47
+ message: 'Chart aggregate dataSource must define groupBy',
80
48
  });
81
49
  }
50
+
51
+ return errors;
82
52
  }
83
53
 
54
+ errors.push({
55
+ field: 'data_source',
56
+ message: 'Chart widget must have aggregate dataSource config',
57
+ });
58
+
84
59
  return errors;
85
60
  }
86
61
 
@@ -90,4 +65,21 @@ export function createWidgetConfigValidatorService(
90
65
  return {
91
66
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
92
67
  };
68
+ }
69
+
70
+ function getAggregateDataSource(dataSource: unknown) {
71
+ if (
72
+ typeof dataSource !== 'object'
73
+ || dataSource === null
74
+ || (dataSource as { type?: string }).type !== 'aggregate'
75
+ || typeof (dataSource as { resourceId?: unknown }).resourceId !== 'string'
76
+ ) {
77
+ return null;
78
+ }
79
+
80
+ return dataSource as {
81
+ type: 'aggregate';
82
+ resourceId: string;
83
+ groupBy?: unknown;
84
+ };
93
85
  }
@@ -1,57 +1,204 @@
1
- import { Sorts } from 'adminforth';
2
- import type { IAdminForth } from 'adminforth';
3
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
4
-
5
- export type DashboardWidgetQueryConfig = {
6
- resource: string;
7
- select?: string[];
8
- order?: {
9
- field: string;
10
- direction: 'asc' | 'desc';
1
+ import { Aggregates, GroupBy, Sorts } from 'adminforth';
2
+ import type {
3
+ IAdminForth,
4
+ IAdminForthAndOrFilter,
5
+ IAdminForthSingleFilter,
6
+ IAdminForthSort,
7
+ } from 'adminforth';
8
+ import type {
9
+ AggregationRule,
10
+ DashboardWidgetConfig,
11
+ DashboardWidgetData,
12
+ GroupByRule,
13
+ WidgetDataSource,
14
+ } from '../custom/model/dashboard.types.js';
15
+
16
+ export type DashboardWidgetDataOptions = {
17
+ pagination?: {
18
+ page: number;
19
+ pageSize: number;
11
20
  };
12
- limit?: number;
13
21
  };
14
22
 
15
- export type DashboardWidgetData = {
16
- columns: string[];
17
- rows: Record<string, unknown>[];
18
- };
23
+ type DashboardWidgetFilters =
24
+ | IAdminForthSingleFilter
25
+ | IAdminForthAndOrFilter
26
+ | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
19
27
 
20
28
  export type WidgetDataService = {
21
- getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
29
+ getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
22
30
  };
23
31
 
24
32
  export async function getWidgetData(
25
33
  adminforth: IAdminForth,
26
34
  widget: DashboardWidgetConfig,
35
+ options: DashboardWidgetDataOptions = {},
27
36
  ): Promise<DashboardWidgetData | null> {
28
- if (!widget.query) {
37
+ const dataSource = getWidgetDataSource(widget.dataSource);
38
+
39
+ if (!dataSource) {
29
40
  return null;
30
41
  }
31
42
 
32
- const query = widget.query as DashboardWidgetQueryConfig;
43
+ if (dataSource.type === 'aggregate') {
44
+ return getAggregateWidgetData(adminforth, dataSource);
45
+ }
33
46
 
34
- const rows = await adminforth.resource(query.resource).list(
35
- [],
36
- query.limit,
37
- 0,
38
- query.order
39
- ? [query.order.direction === 'desc' ? Sorts.DESC(query.order.field) : Sorts.ASC(query.order.field)]
40
- : undefined,
47
+ return getResourceWidgetData(adminforth, dataSource, options);
48
+ }
49
+
50
+ async function getResourceWidgetData(
51
+ adminforth: IAdminForth,
52
+ dataSource: Extract<WidgetDataSource, { type: 'resource' }>,
53
+ options: DashboardWidgetDataOptions,
54
+ ): Promise<DashboardWidgetData> {
55
+ const resource = adminforth.resource(dataSource.resourceId);
56
+ const filters = normalizeFilters(dataSource.filters);
57
+ const sort = normalizeSort(dataSource.sort);
58
+ const pagination = options.pagination;
59
+ const offset = pagination ? (pagination.page - 1) * pagination.pageSize : 0;
60
+ const limit = pagination ? pagination.pageSize : undefined;
61
+
62
+ const rows = await resource.list(
63
+ filters,
64
+ limit ?? undefined,
65
+ offset,
66
+ sort,
41
67
  );
42
68
 
43
- const columns = query.select ?? Object.keys(rows[0] ?? {});
69
+ const columns = dataSource.columns ?? Object.keys(rows[0] ?? {});
70
+ const total = pagination ? await resource.count(filters) : 0;
44
71
 
45
72
  return {
46
73
  columns,
47
74
  rows: rows.map((row) => (
48
75
  Object.fromEntries(columns.map((column) => [column, row[column]]))
49
76
  )),
77
+ ...(pagination ? {
78
+ pagination: {
79
+ page: pagination.page,
80
+ pageSize: pagination.pageSize,
81
+ total,
82
+ totalPages: Math.max(Math.ceil(total / pagination.pageSize), 1),
83
+ },
84
+ } : {}),
85
+ };
86
+ }
87
+
88
+ async function getAggregateWidgetData(
89
+ adminforth: IAdminForth,
90
+ dataSource: Extract<WidgetDataSource, { type: 'aggregate' }>,
91
+ ): Promise<DashboardWidgetData> {
92
+ const resource = adminforth.resource(dataSource.resourceId);
93
+ const rows = await resource.aggregate(
94
+ normalizeFilters(dataSource.filters),
95
+ Object.fromEntries(
96
+ Object.entries(dataSource.aggregations).map(([alias, rule]) => [
97
+ alias,
98
+ createAggregationRule(rule),
99
+ ]),
100
+ ),
101
+ dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined,
102
+ );
103
+ const columns = Object.keys(rows[0] ?? {});
104
+
105
+ if (!dataSource.groupBy) {
106
+ const values = rows[0] ?? {};
107
+
108
+ return {
109
+ kind: 'aggregate',
110
+ columns: Object.keys(values),
111
+ rows: Object.keys(values).length ? [values] : [],
112
+ values,
113
+ };
114
+ }
115
+
116
+ return {
117
+ kind: 'aggregate',
118
+ columns,
119
+ rows,
50
120
  };
51
121
  }
52
122
 
123
+ function getWidgetDataSource(dataSource: unknown): WidgetDataSource | undefined {
124
+ if (isWidgetDataSource(dataSource)) {
125
+ return dataSource;
126
+ }
127
+
128
+ return undefined;
129
+ }
130
+
131
+ function isWidgetDataSource(value: unknown): value is WidgetDataSource {
132
+ return isRecord(value)
133
+ && (value.type === 'resource' || value.type === 'aggregate')
134
+ && typeof value.resourceId === 'string';
135
+ }
136
+
137
+ function normalizeFilters(filters: unknown): DashboardWidgetFilters {
138
+ if (Array.isArray(filters)) {
139
+ return filters as DashboardWidgetFilters;
140
+ }
141
+
142
+ if (isRecord(filters)) {
143
+ return filters as DashboardWidgetFilters;
144
+ }
145
+
146
+ return [];
147
+ }
148
+
149
+ function normalizeSort(sort: unknown): IAdminForthSort | IAdminForthSort[] | undefined {
150
+ if (Array.isArray(sort)) {
151
+ return sort as IAdminForthSort[];
152
+ }
153
+
154
+ if (!isRecord(sort) || typeof sort.field !== 'string') {
155
+ return undefined;
156
+ }
157
+
158
+ if (sort.direction === 'asc') {
159
+ return [Sorts.ASC(sort.field)];
160
+ }
161
+
162
+ if (sort.direction === 'desc') {
163
+ return [Sorts.DESC(sort.field)];
164
+ }
165
+
166
+ return sort as IAdminForthSort;
167
+ }
168
+
169
+ function createAggregationRule(rule: AggregationRule) {
170
+ switch (rule.operation) {
171
+ case 'sum':
172
+ return Aggregates.sum(rule.field!);
173
+ case 'count':
174
+ return Aggregates.count();
175
+ case 'avg':
176
+ return Aggregates.avg(rule.field!);
177
+ case 'min':
178
+ return Aggregates.min(rule.field!);
179
+ case 'max':
180
+ return Aggregates.max(rule.field!);
181
+ case 'median':
182
+ return Aggregates.median(rule.field!);
183
+ default:
184
+ throw new Error(`Unsupported aggregation operation: ${(rule as AggregationRule).operation}`);
185
+ }
186
+ }
187
+
188
+ function createGroupByRule(rule: GroupByRule) {
189
+ if (rule.type === 'field') {
190
+ return GroupBy.Field(rule.field);
191
+ }
192
+
193
+ return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
194
+ }
195
+
196
+ function isRecord(value: unknown): value is Record<string, any> {
197
+ return typeof value === 'object' && value !== null;
198
+ }
199
+
53
200
  export function createWidgetDataService(adminforth: IAdminForth): WidgetDataService {
54
201
  return {
55
- getWidgetData: (widget) => getWidgetData(adminforth, widget),
202
+ getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
56
203
  };
57
- }
204
+ }