@adminforth/dashboard 1.0.0 → 1.1.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 (41) hide show
  1. package/README.md +116 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +158 -1
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/WidgetShell.vue +8 -4
  6. package/custom/widgets/chart/chart.utils.ts +2 -2
  7. package/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
  8. package/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
  9. package/custom/widgets/table/TableWidget.vue +155 -30
  10. package/dist/custom/api/dashboardApi.d.ts +7 -1
  11. package/dist/custom/api/dashboardApi.js +4 -6
  12. package/dist/custom/api/dashboardApi.ts +9 -0
  13. package/dist/custom/model/dashboard.types.d.ts +45 -0
  14. package/dist/custom/model/dashboard.types.js +82 -1
  15. package/dist/custom/model/dashboard.types.ts +158 -1
  16. package/dist/custom/queries/useDashboardConfig.d.ts +42 -0
  17. package/dist/custom/queries/useWidgetData.d.ts +44 -1
  18. package/dist/custom/queries/useWidgetData.js +3 -3
  19. package/dist/custom/queries/useWidgetData.ts +8 -4
  20. package/dist/custom/runtime/WidgetShell.vue +8 -4
  21. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  22. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  23. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  24. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
  25. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
  26. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  27. package/dist/endpoint/widgets.d.ts +6 -1
  28. package/dist/endpoint/widgets.js +22 -4
  29. package/dist/schema/api.d.ts +882 -212
  30. package/dist/schema/api.js +11 -2
  31. package/dist/schema/widget.d.ts +542 -4
  32. package/dist/schema/widget.js +111 -1
  33. package/dist/services/widgetConfigValidator.js +32 -6
  34. package/dist/services/widgetDataService.d.ts +8 -6
  35. package/dist/services/widgetDataService.js +133 -11
  36. package/endpoint/widgets.ts +31 -4
  37. package/package.json +1 -1
  38. package/schema/api.ts +11 -1
  39. package/schema/widget.ts +114 -1
  40. package/services/widgetConfigValidator.ts +45 -6
  41. package/services/widgetDataService.ts +201 -19
@@ -19,18 +19,40 @@ export function validateDashboardWidgetApiConfig(
19
19
 
20
20
  const errors: DashboardWidgetConfigValidationError[] = [];
21
21
 
22
- if (!widget.query) {
22
+ if (!widget.chart) {
23
23
  errors.push({
24
- field: 'query',
25
- message: 'Chart widget must have query config',
24
+ field: 'chart',
25
+ message: 'Chart widget must have chart config',
26
26
  });
27
27
  return errors;
28
28
  }
29
29
 
30
- if (!widget.chart) {
30
+ const aggregateDataSource = getAggregateDataSource(widget.dataSource);
31
+
32
+ if (aggregateDataSource) {
33
+ const resource = adminforth.config.resources.find((item) => item.resourceId === aggregateDataSource.resourceId);
34
+
35
+ if (!resource) {
36
+ errors.push({
37
+ field: 'dataSource.resourceId',
38
+ message: `Resource "${aggregateDataSource.resourceId}" is not registered`,
39
+ });
40
+ }
41
+
42
+ if (!aggregateDataSource.groupBy) {
43
+ errors.push({
44
+ field: 'dataSource.groupBy',
45
+ message: 'Chart aggregate dataSource must define groupBy',
46
+ });
47
+ }
48
+
49
+ return errors;
50
+ }
51
+
52
+ if (!widget.query) {
31
53
  errors.push({
32
- field: 'chart',
33
- message: 'Chart widget must have chart config',
54
+ field: 'query',
55
+ message: 'Chart widget must have query or aggregate dataSource config',
34
56
  });
35
57
  return errors;
36
58
  }
@@ -90,4 +112,21 @@ export function createWidgetConfigValidatorService(
90
112
  return {
91
113
  validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
92
114
  };
115
+ }
116
+
117
+ function getAggregateDataSource(dataSource: unknown) {
118
+ if (
119
+ typeof dataSource !== 'object'
120
+ || dataSource === null
121
+ || (dataSource as { type?: string }).type !== 'aggregate'
122
+ || typeof (dataSource as { resourceId?: unknown }).resourceId !== 'string'
123
+ ) {
124
+ return null;
125
+ }
126
+
127
+ return dataSource as {
128
+ type: 'aggregate';
129
+ resourceId: string;
130
+ groupBy?: unknown;
131
+ };
93
132
  }
@@ -1,6 +1,17 @@
1
- import { Sorts } from 'adminforth';
2
- import type { IAdminForth } from 'adminforth';
3
- import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
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';
4
15
 
5
16
  export type DashboardWidgetQueryConfig = {
6
17
  resource: string;
@@ -12,46 +23,217 @@ export type DashboardWidgetQueryConfig = {
12
23
  limit?: number;
13
24
  };
14
25
 
15
- export type DashboardWidgetData = {
16
- columns: string[];
17
- rows: Record<string, unknown>[];
26
+ export type DashboardWidgetDataOptions = {
27
+ pagination?: {
28
+ page: number;
29
+ pageSize: number;
30
+ };
18
31
  };
19
32
 
33
+ type DashboardWidgetFilters =
34
+ | IAdminForthSingleFilter
35
+ | IAdminForthAndOrFilter
36
+ | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
37
+
20
38
  export type WidgetDataService = {
21
- getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
39
+ getWidgetData: (widget: DashboardWidgetConfig, options?: DashboardWidgetDataOptions) => Promise<DashboardWidgetData | null>;
22
40
  };
23
41
 
24
42
  export async function getWidgetData(
25
43
  adminforth: IAdminForth,
26
44
  widget: DashboardWidgetConfig,
45
+ options: DashboardWidgetDataOptions = {},
27
46
  ): Promise<DashboardWidgetData | null> {
28
- if (!widget.query) {
47
+ const legacyQuery = getLegacyQueryConfig(widget.query);
48
+ const dataSource = getWidgetDataSource(widget, legacyQuery);
49
+
50
+ if (!dataSource) {
29
51
  return null;
30
52
  }
31
53
 
32
- const query = widget.query as DashboardWidgetQueryConfig;
54
+ if (dataSource.type === 'aggregate') {
55
+ return getAggregateWidgetData(adminforth, dataSource);
56
+ }
57
+
58
+ return getResourceWidgetData(adminforth, dataSource, legacyQuery, options);
59
+ }
60
+
61
+ async function getResourceWidgetData(
62
+ adminforth: IAdminForth,
63
+ dataSource: Extract<WidgetDataSource, { type: 'resource' }>,
64
+ legacyQuery: DashboardWidgetQueryConfig | undefined,
65
+ options: DashboardWidgetDataOptions,
66
+ ): Promise<DashboardWidgetData> {
67
+ const resource = adminforth.resource(dataSource.resourceId);
68
+ const filters = normalizeFilters(dataSource.filters);
69
+ const sort = normalizeSort(dataSource.sort ?? legacyQuery?.order);
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;
33
76
 
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,
77
+ const rows = await resource.list(
78
+ filters,
79
+ limit ?? undefined,
80
+ offset,
81
+ sort,
41
82
  );
42
83
 
43
- const columns = query.select ?? Object.keys(rows[0] ?? {});
84
+ const columns = dataSource.columns ?? legacyQuery?.select ?? Object.keys(rows[0] ?? {});
85
+ const total = pagination ? Math.min(await resource.count(filters), queryLimit ?? Infinity) : 0;
44
86
 
45
87
  return {
46
88
  columns,
47
89
  rows: rows.map((row) => (
48
90
  Object.fromEntries(columns.map((column) => [column, row[column]]))
49
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
+ } : {}),
100
+ };
101
+ }
102
+
103
+ async function getAggregateWidgetData(
104
+ adminforth: IAdminForth,
105
+ dataSource: Extract<WidgetDataSource, { type: 'aggregate' }>,
106
+ ): Promise<DashboardWidgetData> {
107
+ const resource = adminforth.resource(dataSource.resourceId);
108
+ const rows = await resource.aggregate(
109
+ normalizeFilters(dataSource.filters),
110
+ Object.fromEntries(
111
+ Object.entries(dataSource.aggregations).map(([alias, rule]) => [
112
+ alias,
113
+ createAggregationRule(rule),
114
+ ]),
115
+ ),
116
+ dataSource.groupBy ? createGroupByRule(dataSource.groupBy) : undefined,
117
+ );
118
+ const columns = Object.keys(rows[0] ?? {});
119
+
120
+ if (!dataSource.groupBy) {
121
+ const values = rows[0] ?? {};
122
+
123
+ return {
124
+ kind: 'aggregate',
125
+ columns: Object.keys(values),
126
+ rows: Object.keys(values).length ? [values] : [],
127
+ values,
128
+ };
129
+ }
130
+
131
+ return {
132
+ kind: 'aggregate',
133
+ columns,
134
+ rows,
135
+ };
136
+ }
137
+
138
+ function getLegacyQueryConfig(query: unknown): DashboardWidgetQueryConfig | undefined {
139
+ if (!isRecord(query) || typeof query.resource !== 'string') {
140
+ return undefined;
141
+ }
142
+
143
+ return query as DashboardWidgetQueryConfig;
144
+ }
145
+
146
+ function getWidgetDataSource(
147
+ widget: DashboardWidgetConfig,
148
+ legacyQuery: DashboardWidgetQueryConfig | undefined,
149
+ ): WidgetDataSource | undefined {
150
+ if (isWidgetDataSource(widget.dataSource)) {
151
+ return widget.dataSource;
152
+ }
153
+
154
+ if (!legacyQuery) {
155
+ return undefined;
156
+ }
157
+
158
+ return {
159
+ type: 'resource',
160
+ resourceId: legacyQuery.resource,
161
+ columns: legacyQuery.select,
162
+ sort: legacyQuery.order,
50
163
  };
51
164
  }
52
165
 
166
+ function isWidgetDataSource(value: unknown): value is WidgetDataSource {
167
+ return isRecord(value)
168
+ && (value.type === 'resource' || value.type === 'aggregate')
169
+ && typeof value.resourceId === 'string';
170
+ }
171
+
172
+ function normalizeFilters(filters: unknown): DashboardWidgetFilters {
173
+ if (Array.isArray(filters)) {
174
+ return filters as DashboardWidgetFilters;
175
+ }
176
+
177
+ if (isRecord(filters)) {
178
+ return filters as DashboardWidgetFilters;
179
+ }
180
+
181
+ return [];
182
+ }
183
+
184
+ function normalizeSort(sort: unknown): IAdminForthSort | IAdminForthSort[] | undefined {
185
+ if (Array.isArray(sort)) {
186
+ return sort as IAdminForthSort[];
187
+ }
188
+
189
+ if (!isRecord(sort) || typeof sort.field !== 'string') {
190
+ return undefined;
191
+ }
192
+
193
+ if (sort.direction === 'asc') {
194
+ return [Sorts.ASC(sort.field)];
195
+ }
196
+
197
+ if (sort.direction === 'desc') {
198
+ return [Sorts.DESC(sort.field)];
199
+ }
200
+
201
+ return sort as IAdminForthSort;
202
+ }
203
+
204
+ function createAggregationRule(rule: AggregationRule) {
205
+ switch (rule.operation) {
206
+ case 'sum':
207
+ return Aggregates.sum(rule.field!);
208
+ case 'count':
209
+ return Aggregates.count();
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}`);
220
+ }
221
+ }
222
+
223
+ function createGroupByRule(rule: GroupByRule) {
224
+ if (rule.type === 'field') {
225
+ return GroupBy.Field(rule.field);
226
+ }
227
+
228
+ return GroupBy.DateTrunc(rule.field, rule.truncation, rule.timezone);
229
+ }
230
+
231
+ function isRecord(value: unknown): value is Record<string, any> {
232
+ return typeof value === 'object' && value !== null;
233
+ }
234
+
53
235
  export function createWidgetDataService(adminforth: IAdminForth): WidgetDataService {
54
236
  return {
55
- getWidgetData: (widget) => getWidgetData(adminforth, widget),
237
+ getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
56
238
  };
57
- }
239
+ }