@adminforth/dashboard 1.5.0 → 1.7.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/custom/api/dashboardApi.ts +137 -1
- package/custom/model/dashboard.types.ts +32 -22
- package/custom/package.json +1 -0
- package/custom/pnpm-lock.yaml +31 -0
- package/custom/runtime/DashboardRuntime.vue +9 -15
- package/custom/runtime/YamlConfigEditor.vue +109 -0
- package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/custom/widgets/KpiCardWidget.vue +172 -9
- package/custom/widgets/chart/ChartWidget.vue +5 -5
- package/custom/widgets/registry.ts +4 -4
- package/dist/custom/api/dashboardApi.d.ts +46 -1
- package/dist/custom/api/dashboardApi.js +90 -0
- package/dist/custom/api/dashboardApi.ts +137 -1
- package/dist/custom/model/dashboard.types.d.ts +30 -14
- package/dist/custom/model/dashboard.types.js +2 -2
- package/dist/custom/model/dashboard.types.ts +32 -22
- package/dist/custom/package.json +1 -0
- package/dist/custom/pnpm-lock.yaml +31 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
- package/dist/custom/queries/useWidgetData.d.ts +106 -104
- package/dist/custom/runtime/DashboardRuntime.vue +9 -15
- package/dist/custom/runtime/YamlConfigEditor.vue +109 -0
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
- package/dist/custom/widgets/KpiCardWidget.vue +172 -9
- package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
- package/dist/custom/widgets/registry.js +4 -4
- package/dist/custom/widgets/registry.ts +4 -4
- package/dist/endpoint/widgets.js +99 -14
- package/dist/schema/api.d.ts +11426 -1634
- package/dist/schema/api.js +118 -21
- package/dist/schema/widget.d.ts +425 -1980
- package/dist/schema/widget.js +13 -374
- package/dist/schema/widgets/charts.d.ts +1689 -0
- package/dist/schema/widgets/charts.js +92 -0
- package/dist/schema/widgets/common.d.ts +275 -0
- package/dist/schema/widgets/common.js +171 -0
- package/dist/schema/widgets/gauge-card.d.ts +172 -0
- package/dist/schema/widgets/gauge-card.js +28 -0
- package/dist/schema/widgets/kpi-card.d.ts +212 -0
- package/dist/schema/widgets/kpi-card.js +43 -0
- package/dist/schema/widgets/pivot-table.d.ts +196 -0
- package/dist/schema/widgets/pivot-table.js +17 -0
- package/dist/schema/widgets/table.d.ts +130 -0
- package/dist/schema/widgets/table.js +12 -0
- package/dist/services/widgetDataService.js +96 -2
- package/endpoint/widgets.ts +173 -26
- package/package.json +1 -1
- package/schema/api.ts +148 -22
- package/schema/widget.ts +43 -425
- package/schema/widgets/charts.ts +113 -0
- package/schema/widgets/common.ts +194 -0
- package/schema/widgets/gauge-card.ts +34 -0
- package/schema/widgets/kpi-card.ts +49 -0
- package/schema/widgets/pivot-table.ts +24 -0
- package/schema/widgets/table.ts +18 -0
- package/services/widgetDataService.ts +129 -3
- package/shims-vue.d.ts +11 -0
- package/tsconfig.json +3 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const TableViewConfigSchema: z.ZodObject<{
|
|
3
|
+
columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
4
|
+
field: z.ZodString;
|
|
5
|
+
label: z.ZodOptional<z.ZodString>;
|
|
6
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
number: "number";
|
|
8
|
+
integer: "integer";
|
|
9
|
+
compact_number: "compact_number";
|
|
10
|
+
currency: "currency";
|
|
11
|
+
percent: "percent";
|
|
12
|
+
percent_delta: "percent_delta";
|
|
13
|
+
number_delta: "number_delta";
|
|
14
|
+
currency_delta: "currency_delta";
|
|
15
|
+
}>>;
|
|
16
|
+
}, z.core.$strict>>>;
|
|
17
|
+
pagination: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
page_size: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
export declare const TableWidgetConfigSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
group_id: z.ZodString;
|
|
23
|
+
order: z.ZodNumber;
|
|
24
|
+
label: z.ZodOptional<z.ZodString>;
|
|
25
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
26
|
+
size: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
small: "small";
|
|
28
|
+
medium: "medium";
|
|
29
|
+
large: "large";
|
|
30
|
+
wide: "wide";
|
|
31
|
+
full: "full";
|
|
32
|
+
}>>;
|
|
33
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
min_width: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
max_width: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
37
|
+
target: z.ZodLiteral<"table">;
|
|
38
|
+
table: z.ZodOptional<z.ZodObject<{
|
|
39
|
+
columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
40
|
+
field: z.ZodString;
|
|
41
|
+
label: z.ZodOptional<z.ZodString>;
|
|
42
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
number: "number";
|
|
44
|
+
integer: "integer";
|
|
45
|
+
compact_number: "compact_number";
|
|
46
|
+
currency: "currency";
|
|
47
|
+
percent: "percent";
|
|
48
|
+
percent_delta: "percent_delta";
|
|
49
|
+
number_delta: "number_delta";
|
|
50
|
+
currency_delta: "currency_delta";
|
|
51
|
+
}>>;
|
|
52
|
+
}, z.core.$strict>>>;
|
|
53
|
+
pagination: z.ZodOptional<z.ZodBoolean>;
|
|
54
|
+
page_size: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
}, z.core.$strict>>;
|
|
56
|
+
query: z.ZodObject<{
|
|
57
|
+
resource: z.ZodString;
|
|
58
|
+
select: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
59
|
+
field: z.ZodString;
|
|
60
|
+
as: z.ZodOptional<z.ZodString>;
|
|
61
|
+
grain: z.ZodOptional<z.ZodEnum<{
|
|
62
|
+
day: "day";
|
|
63
|
+
week: "week";
|
|
64
|
+
month: "month";
|
|
65
|
+
year: "year";
|
|
66
|
+
}>>;
|
|
67
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
68
|
+
agg: z.ZodEnum<{
|
|
69
|
+
sum: "sum";
|
|
70
|
+
count: "count";
|
|
71
|
+
count_distinct: "count_distinct";
|
|
72
|
+
avg: "avg";
|
|
73
|
+
min: "min";
|
|
74
|
+
max: "max";
|
|
75
|
+
median: "median";
|
|
76
|
+
}>;
|
|
77
|
+
field: z.ZodOptional<z.ZodString>;
|
|
78
|
+
as: z.ZodString;
|
|
79
|
+
filters: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
80
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
81
|
+
calc: z.ZodString;
|
|
82
|
+
as: z.ZodString;
|
|
83
|
+
}, z.core.$strict>]>>>;
|
|
84
|
+
sparkline: z.ZodOptional<z.ZodObject<{
|
|
85
|
+
field: z.ZodString;
|
|
86
|
+
grain: z.ZodEnum<{
|
|
87
|
+
day: "day";
|
|
88
|
+
week: "week";
|
|
89
|
+
month: "month";
|
|
90
|
+
year: "year";
|
|
91
|
+
}>;
|
|
92
|
+
as: z.ZodString;
|
|
93
|
+
fill_missing: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
94
|
+
}, z.core.$strict>>;
|
|
95
|
+
filters: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
96
|
+
group_by: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
97
|
+
field: z.ZodString;
|
|
98
|
+
as: z.ZodOptional<z.ZodString>;
|
|
99
|
+
grain: z.ZodOptional<z.ZodEnum<{
|
|
100
|
+
day: "day";
|
|
101
|
+
week: "week";
|
|
102
|
+
month: "month";
|
|
103
|
+
year: "year";
|
|
104
|
+
}>>;
|
|
105
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
106
|
+
}, z.core.$strict>]>>>;
|
|
107
|
+
order_by: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
108
|
+
field: z.ZodString;
|
|
109
|
+
direction: z.ZodOptional<z.ZodEnum<{
|
|
110
|
+
asc: "asc";
|
|
111
|
+
desc: "desc";
|
|
112
|
+
}>>;
|
|
113
|
+
}, z.core.$strict>>>;
|
|
114
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
115
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
116
|
+
bucket: z.ZodOptional<z.ZodObject<{
|
|
117
|
+
field: z.ZodString;
|
|
118
|
+
buckets: z.ZodArray<z.ZodObject<{
|
|
119
|
+
label: z.ZodString;
|
|
120
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
122
|
+
}, z.core.$strict>>;
|
|
123
|
+
}, z.core.$strict>>;
|
|
124
|
+
calcs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
125
|
+
calc: z.ZodString;
|
|
126
|
+
as: z.ZodString;
|
|
127
|
+
}, z.core.$strict>>>;
|
|
128
|
+
formatting: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
129
|
+
}, z.core.$strict>;
|
|
130
|
+
}, z.core.$strict>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ChartFieldRefSchema, QueryConfigSchema, WidgetBaseSchema, } from './common.js';
|
|
3
|
+
export const TableViewConfigSchema = z.object({
|
|
4
|
+
columns: z.array(ChartFieldRefSchema).optional(),
|
|
5
|
+
pagination: z.boolean().optional(),
|
|
6
|
+
page_size: z.number().int().positive().optional(),
|
|
7
|
+
}).strict();
|
|
8
|
+
export const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
9
|
+
target: z.literal('table'),
|
|
10
|
+
table: TableViewConfigSchema.optional(),
|
|
11
|
+
query: QueryConfigSchema,
|
|
12
|
+
});
|
|
@@ -11,7 +11,8 @@ import { Filters, Sorts } from 'adminforth';
|
|
|
11
11
|
const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
|
|
12
12
|
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;
|
|
13
13
|
const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
|
|
14
|
-
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s]+$/;
|
|
14
|
+
const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
|
|
15
|
+
const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
|
|
15
16
|
const FILTER_OPERATORS = {
|
|
16
17
|
eq: Filters.EQ,
|
|
17
18
|
neq: Filters.NEQ,
|
|
@@ -78,6 +79,10 @@ function getFunnelWidgetData(adminforth, query, variables) {
|
|
|
78
79
|
function getQueryWidgetData(adminforth, query, variables) {
|
|
79
80
|
return __awaiter(this, void 0, void 0, function* () {
|
|
80
81
|
var _a, _b, _c;
|
|
82
|
+
const metricSelect = getSingleAggregateMetricSelect(query);
|
|
83
|
+
if (metricSelect) {
|
|
84
|
+
return getMetricWidgetData(adminforth, query, metricSelect);
|
|
85
|
+
}
|
|
81
86
|
const selectedRows = isAggregateQuery(query)
|
|
82
87
|
? yield buildAggregateQueryRows(adminforth, query, variables)
|
|
83
88
|
: buildPlainQueryRows(yield getResourceRows(adminforth, query.resource, query.filters, getBackendSort(query.order_by)), query, variables);
|
|
@@ -97,6 +102,43 @@ function getQueryWidgetData(adminforth, query, variables) {
|
|
|
97
102
|
};
|
|
98
103
|
});
|
|
99
104
|
}
|
|
105
|
+
function getMetricWidgetData(adminforth, query, metric) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
var _a;
|
|
108
|
+
const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [metric], []);
|
|
109
|
+
const values = {
|
|
110
|
+
[metric.as]: (_a = currentValues[metric.as]) !== null && _a !== void 0 ? _a : 0,
|
|
111
|
+
};
|
|
112
|
+
const rows = query.sparkline
|
|
113
|
+
? yield getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
|
|
114
|
+
: [values];
|
|
115
|
+
const columns = Array.from(new Set([
|
|
116
|
+
metric.as,
|
|
117
|
+
...(query.sparkline ? [query.sparkline.as] : []),
|
|
118
|
+
]));
|
|
119
|
+
return {
|
|
120
|
+
kind: 'aggregate',
|
|
121
|
+
columns,
|
|
122
|
+
rows,
|
|
123
|
+
values,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function getMetricSparklineRows(adminforth, query, metric, filters) {
|
|
128
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const sparkline = query.sparkline;
|
|
130
|
+
const groupBy = [{
|
|
131
|
+
field: sparkline.field,
|
|
132
|
+
as: sparkline.as,
|
|
133
|
+
grain: sparkline.grain,
|
|
134
|
+
}];
|
|
135
|
+
const rows = yield getAggregateRows(adminforth, query.resource, filters, [metric], groupBy);
|
|
136
|
+
return rows.map((row) => {
|
|
137
|
+
var _a;
|
|
138
|
+
return (Object.assign(Object.assign({}, (_a = query.sparkline) === null || _a === void 0 ? void 0 : _a.fill_missing), row));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
100
142
|
function getResourceRows(adminforth, resourceId, filters, sort) {
|
|
101
143
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
144
|
return adminforth.resource(resourceId).list(getAdminForthFilters(filters), undefined, 0, sort);
|
|
@@ -238,6 +280,18 @@ function isAggregateQuery(query) {
|
|
|
238
280
|
return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
|
|
239
281
|
|| ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
|
|
240
282
|
}
|
|
283
|
+
function getSingleAggregateMetricSelect(query) {
|
|
284
|
+
var _a, _b;
|
|
285
|
+
if ((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const select = (_b = query.select) !== null && _b !== void 0 ? _b : [];
|
|
289
|
+
const aggregateItems = select.filter(isAggregateSelectItem);
|
|
290
|
+
if (aggregateItems.length !== 1 || aggregateItems.length !== select.length) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return aggregateItems[0];
|
|
294
|
+
}
|
|
241
295
|
function isFieldSelectItem(item) {
|
|
242
296
|
return 'field' in item && !('agg' in item);
|
|
243
297
|
}
|
|
@@ -454,11 +508,51 @@ function toAdminForthFilter(filter) {
|
|
|
454
508
|
}
|
|
455
509
|
for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
|
|
456
510
|
if (Object.prototype.hasOwnProperty.call(filter, operator)) {
|
|
457
|
-
return createFilter(filter.field, filter[operator]);
|
|
511
|
+
return createFilter(filter.field, resolveFilterValue(filter[operator]));
|
|
458
512
|
}
|
|
459
513
|
}
|
|
460
514
|
return Filters.AND([]);
|
|
461
515
|
}
|
|
516
|
+
function resolveFilterValue(value) {
|
|
517
|
+
if (Array.isArray(value)) {
|
|
518
|
+
return value.map((item) => resolveFilterValue(item));
|
|
519
|
+
}
|
|
520
|
+
if (!isRecord(value)) {
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
if (value.now === true) {
|
|
524
|
+
return new Date().toISOString();
|
|
525
|
+
}
|
|
526
|
+
if (typeof value.now_minus === 'string') {
|
|
527
|
+
return subtractDuration(new Date(), value.now_minus).toISOString();
|
|
528
|
+
}
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function subtractDuration(now, duration) {
|
|
532
|
+
const match = duration.match(RELATIVE_DURATION_RE);
|
|
533
|
+
if (!match) {
|
|
534
|
+
throw new Error(`Unsupported relative date duration: ${duration}`);
|
|
535
|
+
}
|
|
536
|
+
const amount = Number(match[1]);
|
|
537
|
+
const unit = match[2];
|
|
538
|
+
const date = new Date(now);
|
|
539
|
+
if (unit === 'h') {
|
|
540
|
+
date.setUTCHours(date.getUTCHours() - amount);
|
|
541
|
+
}
|
|
542
|
+
else if (unit === 'd') {
|
|
543
|
+
date.setUTCDate(date.getUTCDate() - amount);
|
|
544
|
+
}
|
|
545
|
+
else if (unit === 'w') {
|
|
546
|
+
date.setUTCDate(date.getUTCDate() - amount * 7);
|
|
547
|
+
}
|
|
548
|
+
else if (unit === 'mo') {
|
|
549
|
+
date.setUTCMonth(date.getUTCMonth() - amount);
|
|
550
|
+
}
|
|
551
|
+
else if (unit === 'y') {
|
|
552
|
+
date.setUTCFullYear(date.getUTCFullYear() - amount);
|
|
553
|
+
}
|
|
554
|
+
return date;
|
|
555
|
+
}
|
|
462
556
|
function toFiniteNumber(value) {
|
|
463
557
|
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
464
558
|
return Number.isFinite(numberValue) ? numberValue : 0;
|
package/endpoint/widgets.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import type {
|
|
4
|
+
ChartDashboardWidgetConfig,
|
|
4
5
|
DashboardConfig,
|
|
5
6
|
DashboardVariables,
|
|
6
7
|
DashboardWidgetConfig,
|
|
7
|
-
|
|
8
|
+
GaugeCardWidgetConfig,
|
|
9
|
+
KpiCardWidgetConfig,
|
|
10
|
+
PivotTableWidgetConfig,
|
|
11
|
+
TableWidgetConfig,
|
|
8
12
|
} from '../custom/model/dashboard.types.js';
|
|
9
13
|
import {
|
|
14
|
+
ConfigureBarChartWidgetRequestSchema,
|
|
15
|
+
ConfigureFunnelChartWidgetRequestSchema,
|
|
16
|
+
ConfigureGaugeCardWidgetRequestSchema,
|
|
17
|
+
ConfigureHistogramChartWidgetRequestSchema,
|
|
18
|
+
ConfigureKpiCardWidgetRequestSchema,
|
|
19
|
+
ConfigureLineChartWidgetRequestSchema,
|
|
20
|
+
ConfigurePieChartWidgetRequestSchema,
|
|
21
|
+
ConfigurePivotTableWidgetRequestSchema,
|
|
22
|
+
ConfigureStackedBarChartWidgetRequestSchema,
|
|
23
|
+
ConfigureTableWidgetRequestSchema,
|
|
10
24
|
DashboardApiResponseSchema,
|
|
11
25
|
DashboardWidgetDataResponseSchema,
|
|
12
26
|
GroupIdRequestSchema,
|
|
@@ -17,6 +31,21 @@ import {
|
|
|
17
31
|
} from '../schema/api.js';
|
|
18
32
|
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
19
33
|
|
|
34
|
+
type ConfigurableWidgetConfig =
|
|
35
|
+
| Omit<TableWidgetConfig, 'id' | 'group_id' | 'order'>
|
|
36
|
+
| Omit<KpiCardWidgetConfig, 'id' | 'group_id' | 'order'>
|
|
37
|
+
| Omit<GaugeCardWidgetConfig, 'id' | 'group_id' | 'order'>
|
|
38
|
+
| Omit<ChartDashboardWidgetConfig, 'id' | 'group_id' | 'order'>
|
|
39
|
+
| Omit<PivotTableWidgetConfig, 'id' | 'group_id' | 'order'>;
|
|
40
|
+
|
|
41
|
+
type ConfigureWidgetRequest = {
|
|
42
|
+
slug: string;
|
|
43
|
+
widgetId: string;
|
|
44
|
+
config: ConfigurableWidgetConfig;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type EndpointRequestSchema = Parameters<IHttpServer['endpoint']>[0]['request_schema'];
|
|
48
|
+
|
|
20
49
|
type WidgetEndpointsContext = {
|
|
21
50
|
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
22
51
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
@@ -38,6 +67,86 @@ type WidgetEndpointsContext = {
|
|
|
38
67
|
) => Promise<unknown>;
|
|
39
68
|
};
|
|
40
69
|
|
|
70
|
+
async function replaceWidgetConfig(
|
|
71
|
+
ctx: WidgetEndpointsContext,
|
|
72
|
+
slug: string,
|
|
73
|
+
widgetId: string,
|
|
74
|
+
widgetConfig: ConfigurableWidgetConfig,
|
|
75
|
+
) {
|
|
76
|
+
let mutationError: string | null = null;
|
|
77
|
+
const updatedDashboard = await ctx.updateDashboardConfig(slug, (config) => {
|
|
78
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
79
|
+
|
|
80
|
+
if (!widget) {
|
|
81
|
+
mutationError = 'Dashboard widget not found';
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nextWidget: DashboardWidgetConfig = {
|
|
86
|
+
...widgetConfig,
|
|
87
|
+
id: widget.id,
|
|
88
|
+
group_id: widget.group_id,
|
|
89
|
+
order: widget.order,
|
|
90
|
+
} as DashboardWidgetConfig;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...config,
|
|
94
|
+
widgets: config.widgets.map((item) => item.id === widgetId
|
|
95
|
+
? nextWidget
|
|
96
|
+
: item),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
updatedDashboard,
|
|
102
|
+
mutationError,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function registerConfigureWidgetEndpoint(
|
|
107
|
+
server: IHttpServer,
|
|
108
|
+
ctx: WidgetEndpointsContext,
|
|
109
|
+
options: {
|
|
110
|
+
path: string;
|
|
111
|
+
description: string;
|
|
112
|
+
requestSchema: EndpointRequestSchema;
|
|
113
|
+
},
|
|
114
|
+
) {
|
|
115
|
+
server.endpoint({
|
|
116
|
+
method: 'POST',
|
|
117
|
+
path: options.path,
|
|
118
|
+
description: options.description,
|
|
119
|
+
request_schema: options.requestSchema,
|
|
120
|
+
response_schema: DashboardApiResponseSchema,
|
|
121
|
+
handler: async ({ body, adminUser, response }) => {
|
|
122
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
123
|
+
response.setStatus(403);
|
|
124
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const request = body as ConfigureWidgetRequest;
|
|
128
|
+
const { updatedDashboard, mutationError } = await replaceWidgetConfig(
|
|
129
|
+
ctx,
|
|
130
|
+
request.slug,
|
|
131
|
+
request.widgetId,
|
|
132
|
+
request.config,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (!updatedDashboard) {
|
|
136
|
+
response.setStatus(404);
|
|
137
|
+
return { error: 'Dashboard not found' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (mutationError) {
|
|
141
|
+
response.setStatus(404);
|
|
142
|
+
return { error: mutationError };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return updatedDashboard;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
41
150
|
export function registerWidgetEndpoints(
|
|
42
151
|
server: IHttpServer,
|
|
43
152
|
ctx: WidgetEndpointsContext,
|
|
@@ -193,6 +302,7 @@ export function registerWidgetEndpoints(
|
|
|
193
302
|
},
|
|
194
303
|
});
|
|
195
304
|
|
|
305
|
+
|
|
196
306
|
server.endpoint({
|
|
197
307
|
method: 'POST',
|
|
198
308
|
path: '/dashboard/set_widget_config',
|
|
@@ -205,31 +315,8 @@ export function registerWidgetEndpoints(
|
|
|
205
315
|
return { error: 'Dashboard edit is not allowed' };
|
|
206
316
|
}
|
|
207
317
|
|
|
208
|
-
|
|
209
|
-
const updatedDashboard = await ctx.
|
|
210
|
-
const widget = config.widgets.find((item) => item.id === body.widgetId);
|
|
211
|
-
|
|
212
|
-
if (!widget) {
|
|
213
|
-
mutationError = 'Dashboard widget not found';
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const typedWidgetConfig = body.config as EditableDashboardWidgetConfig;
|
|
218
|
-
|
|
219
|
-
const nextWidget: DashboardWidgetConfig = {
|
|
220
|
-
...typedWidgetConfig,
|
|
221
|
-
id: widget.id,
|
|
222
|
-
group_id: widget.group_id,
|
|
223
|
-
order: widget.order,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
...config,
|
|
228
|
-
widgets: config.widgets.map((item) => item.id === body.widgetId
|
|
229
|
-
? nextWidget
|
|
230
|
-
: item),
|
|
231
|
-
};
|
|
232
|
-
});
|
|
318
|
+
const request = body as ConfigureWidgetRequest;
|
|
319
|
+
const { updatedDashboard, mutationError } = await replaceWidgetConfig(ctx, request.slug, request.widgetId, request.config);
|
|
233
320
|
|
|
234
321
|
if (!updatedDashboard) {
|
|
235
322
|
response.setStatus(404);
|
|
@@ -245,6 +332,66 @@ export function registerWidgetEndpoints(
|
|
|
245
332
|
},
|
|
246
333
|
});
|
|
247
334
|
|
|
335
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
336
|
+
path: '/dashboard/configure_table_widget',
|
|
337
|
+
description: 'Configures an existing dashboard widget as a table. Superadmin only.',
|
|
338
|
+
requestSchema: ConfigureTableWidgetRequestSchema,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
342
|
+
path: '/dashboard/configure_kpi_card_widget',
|
|
343
|
+
description: 'Configures an existing dashboard widget as a KPI card. Superadmin only.',
|
|
344
|
+
requestSchema: ConfigureKpiCardWidgetRequestSchema,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
348
|
+
path: '/dashboard/configure_gauge_card_widget',
|
|
349
|
+
description: 'Configures an existing dashboard widget as a gauge card. Superadmin only.',
|
|
350
|
+
requestSchema: ConfigureGaugeCardWidgetRequestSchema,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
354
|
+
path: '/dashboard/configure_pivot_table_widget',
|
|
355
|
+
description: 'Configures an existing dashboard widget as a pivot table. Superadmin only.',
|
|
356
|
+
requestSchema: ConfigurePivotTableWidgetRequestSchema,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
360
|
+
path: '/dashboard/configure_line_chart_widget',
|
|
361
|
+
description: 'Configures an existing dashboard widget as a line chart. Superadmin only.',
|
|
362
|
+
requestSchema: ConfigureLineChartWidgetRequestSchema,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
366
|
+
path: '/dashboard/configure_bar_chart_widget',
|
|
367
|
+
description: 'Configures an existing dashboard widget as a bar chart. Superadmin only.',
|
|
368
|
+
requestSchema: ConfigureBarChartWidgetRequestSchema,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
372
|
+
path: '/dashboard/configure_stacked_bar_chart_widget',
|
|
373
|
+
description: 'Configures an existing dashboard widget as a stacked bar chart. Superadmin only.',
|
|
374
|
+
requestSchema: ConfigureStackedBarChartWidgetRequestSchema,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
378
|
+
path: '/dashboard/configure_pie_chart_widget',
|
|
379
|
+
description: 'Configures an existing dashboard widget as a pie chart. Superadmin only.',
|
|
380
|
+
requestSchema: ConfigurePieChartWidgetRequestSchema,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
384
|
+
path: '/dashboard/configure_histogram_chart_widget',
|
|
385
|
+
description: 'Configures an existing dashboard widget as a histogram chart. Superadmin only.',
|
|
386
|
+
requestSchema: ConfigureHistogramChartWidgetRequestSchema,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
registerConfigureWidgetEndpoint(server, ctx, {
|
|
390
|
+
path: '/dashboard/configure_funnel_chart_widget',
|
|
391
|
+
description: 'Configures an existing dashboard widget as a funnel chart. Superadmin only.',
|
|
392
|
+
requestSchema: ConfigureFunnelChartWidgetRequestSchema,
|
|
393
|
+
});
|
|
394
|
+
|
|
248
395
|
server.endpoint({
|
|
249
396
|
method: 'POST',
|
|
250
397
|
path: '/dashboard/get_dashboard_widget_data',
|