@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.
- package/README.md +116 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +158 -1
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
- package/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- package/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/custom/api/dashboardApi.d.ts +7 -1
- package/dist/custom/api/dashboardApi.js +4 -6
- package/dist/custom/api/dashboardApi.ts +9 -0
- package/dist/custom/model/dashboard.types.d.ts +45 -0
- package/dist/custom/model/dashboard.types.js +82 -1
- package/dist/custom/model/dashboard.types.ts +158 -1
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -0
- package/dist/custom/queries/useWidgetData.d.ts +44 -1
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
- package/dist/custom/widgets/chart/chart.utils.js +2 -2
- package/dist/custom/widgets/chart/chart.utils.ts +2 -2
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +22 -4
- package/dist/schema/api.d.ts +882 -212
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +542 -4
- package/dist/schema/widget.js +111 -1
- package/dist/services/widgetConfigValidator.js +32 -6
- package/dist/services/widgetDataService.d.ts +8 -6
- package/dist/services/widgetDataService.js +133 -11
- package/endpoint/widgets.ts +31 -4
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +114 -1
- package/services/widgetConfigValidator.ts +45 -6
- 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.
|
|
22
|
+
if (!widget.chart) {
|
|
23
23
|
errors.push({
|
|
24
|
-
field: '
|
|
25
|
-
message: 'Chart widget must have
|
|
24
|
+
field: 'chart',
|
|
25
|
+
message: 'Chart widget must have chart config',
|
|
26
26
|
});
|
|
27
27
|
return errors;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
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: '
|
|
33
|
-
message: 'Chart widget must have
|
|
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 {
|
|
3
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
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
|
+
}
|