@adminforth/dashboard 1.4.2 → 1.6.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 -5
- package/custom/model/dashboard.types.ts +32 -22
- package/custom/runtime/DashboardRuntime.vue +2 -3
- package/custom/skills/adminforth-dashboard/SKILL.md +165 -179
- 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 -2
- package/dist/custom/api/dashboardApi.js +90 -5
- package/dist/custom/api/dashboardApi.ts +137 -5
- 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/queries/useDashboardConfig.d.ts +106 -104
- package/dist/custom/queries/useWidgetData.d.ts +106 -104
- package/dist/custom/runtime/DashboardRuntime.vue +2 -3
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +165 -179
- 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/dashboard.d.ts +2 -4
- package/dist/endpoint/dashboard.js +1 -21
- package/dist/endpoint/groups.d.ts +1 -0
- package/dist/endpoint/groups.js +61 -48
- package/dist/endpoint/widgets.d.ts +1 -0
- package/dist/endpoint/widgets.js +167 -64
- package/dist/schema/api.d.ts +11710 -2785
- package/dist/schema/api.js +118 -26
- 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/dashboardConfigService.d.ts +4 -0
- package/dist/services/dashboardConfigService.js +46 -0
- package/dist/services/widgetDataService.js +96 -2
- package/endpoint/dashboard.ts +2 -33
- package/endpoint/groups.ts +91 -72
- package/endpoint/widgets.ts +260 -87
- package/package.json +1 -1
- package/schema/api.ts +148 -28
- 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/dashboardConfigService.ts +73 -0
- package/services/widgetDataService.ts +129 -3
|
@@ -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
|
+
});
|
|
@@ -15,11 +15,15 @@ export type PersistedDashboardResponse = {
|
|
|
15
15
|
revision: number;
|
|
16
16
|
config: DashboardConfig;
|
|
17
17
|
};
|
|
18
|
+
type DashboardConfigMutator = (config: DashboardConfig, dashboard: DashboardRecord) => DashboardConfig | null | Promise<DashboardConfig | null>;
|
|
18
19
|
export declare function getDashboardRecord(adminforth: IAdminForth, dashboardConfigsResourceId: string, slug: string): Promise<DashboardRecord | null>;
|
|
19
20
|
export declare function persistDashboardConfig(adminforth: IAdminForth, dashboardConfigsResourceId: string, dashboard: DashboardRecord, config: DashboardConfig): Promise<PersistedDashboardResponse>;
|
|
21
|
+
export declare function updateDashboardConfig(adminforth: IAdminForth, dashboardConfigsResourceId: string, slug: string, mutateConfig: DashboardConfigMutator): Promise<PersistedDashboardResponse | null>;
|
|
20
22
|
export type DashboardConfigService = {
|
|
21
23
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
22
24
|
parseStoredDashboardConfig: typeof parseStoredDashboardConfig;
|
|
23
25
|
persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
|
|
26
|
+
updateDashboardConfig: (slug: string, mutateConfig: DashboardConfigMutator) => Promise<PersistedDashboardResponse | null>;
|
|
24
27
|
};
|
|
25
28
|
export declare function createDashboardConfigService(adminforth: IAdminForth, dashboardConfigsResourceId: string): DashboardConfigService;
|
|
29
|
+
export {};
|
|
@@ -14,6 +14,29 @@ export function parseStoredDashboardConfig(config) {
|
|
|
14
14
|
const parsedConfig = typeof config === 'string' ? JSON.parse(config) : config;
|
|
15
15
|
return DashboardConfigZodSchema.parse(parsedConfig);
|
|
16
16
|
}
|
|
17
|
+
const dashboardConfigUpdateQueues = new Map();
|
|
18
|
+
function runDashboardConfigUpdateQueued(dashboardSlug, callback) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
var _a;
|
|
21
|
+
const previousUpdate = (_a = dashboardConfigUpdateQueues.get(dashboardSlug)) !== null && _a !== void 0 ? _a : Promise.resolve();
|
|
22
|
+
let releaseCurrentUpdate;
|
|
23
|
+
const currentUpdate = new Promise((resolve) => {
|
|
24
|
+
releaseCurrentUpdate = resolve;
|
|
25
|
+
});
|
|
26
|
+
const queuedUpdate = previousUpdate.then(() => currentUpdate, () => currentUpdate);
|
|
27
|
+
dashboardConfigUpdateQueues.set(dashboardSlug, queuedUpdate);
|
|
28
|
+
yield previousUpdate.catch(() => undefined);
|
|
29
|
+
try {
|
|
30
|
+
return yield callback();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
releaseCurrentUpdate();
|
|
34
|
+
if (dashboardConfigUpdateQueues.get(dashboardSlug) === queuedUpdate) {
|
|
35
|
+
dashboardConfigUpdateQueues.delete(dashboardSlug);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
17
40
|
function normalizeDashboardOrder(config) {
|
|
18
41
|
var _a;
|
|
19
42
|
const widgetsByGroupId = new Map();
|
|
@@ -55,10 +78,33 @@ export function persistDashboardConfig(adminforth, dashboardConfigsResourceId, d
|
|
|
55
78
|
};
|
|
56
79
|
});
|
|
57
80
|
}
|
|
81
|
+
export function updateDashboardConfig(adminforth, dashboardConfigsResourceId, slug, mutateConfig) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
return runDashboardConfigUpdateQueued(slug, () => __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
const dashboard = yield getDashboardRecord(adminforth, dashboardConfigsResourceId, slug);
|
|
85
|
+
if (!dashboard) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const config = parseStoredDashboardConfig(dashboard.config);
|
|
89
|
+
const nextConfig = yield mutateConfig(config, dashboard);
|
|
90
|
+
if (nextConfig === null) {
|
|
91
|
+
return {
|
|
92
|
+
id: dashboard.id,
|
|
93
|
+
slug: dashboard.slug,
|
|
94
|
+
label: dashboard.label,
|
|
95
|
+
revision: dashboard.revision,
|
|
96
|
+
config,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, nextConfig);
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
58
103
|
export function createDashboardConfigService(adminforth, dashboardConfigsResourceId) {
|
|
59
104
|
return {
|
|
60
105
|
getDashboardRecord: (slug) => getDashboardRecord(adminforth, dashboardConfigsResourceId, slug),
|
|
61
106
|
parseStoredDashboardConfig,
|
|
62
107
|
persistDashboardConfig: (dashboard, config) => persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, config),
|
|
108
|
+
updateDashboardConfig: (slug, mutateConfig) => updateDashboardConfig(adminforth, dashboardConfigsResourceId, slug, mutateConfig),
|
|
63
109
|
};
|
|
64
110
|
}
|
|
@@ -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/dashboard.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IHttpServer } from 'adminforth';
|
|
2
2
|
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
3
3
|
import {
|
|
4
4
|
DashboardApiResponseSchema,
|
|
5
|
-
SetDashboardConfigRequestSchema,
|
|
6
5
|
SlugRequestSchema,
|
|
7
6
|
} from '../schema/api.js';
|
|
8
|
-
import type { DashboardRecord
|
|
7
|
+
import type { DashboardRecord } from '../services/dashboardConfigService.js';
|
|
9
8
|
|
|
10
9
|
type DashboardEndpointsContext = {
|
|
11
|
-
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
12
10
|
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
13
11
|
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
14
|
-
persistDashboardConfig: (
|
|
15
|
-
dashboard: DashboardRecord,
|
|
16
|
-
config: DashboardConfig,
|
|
17
|
-
) => Promise<PersistedDashboardResponse>;
|
|
18
12
|
};
|
|
19
13
|
|
|
20
14
|
export function registerDashboardEndpoints(
|
|
@@ -44,29 +38,4 @@ export function registerDashboardEndpoints(
|
|
|
44
38
|
};
|
|
45
39
|
},
|
|
46
40
|
});
|
|
47
|
-
|
|
48
|
-
server.endpoint({
|
|
49
|
-
method: 'POST',
|
|
50
|
-
path: '/dashboard/set_dashboard_config',
|
|
51
|
-
description: 'Replaces one dashboard configuration, including groups and widgets. Superadmin only.',
|
|
52
|
-
request_schema: SetDashboardConfigRequestSchema,
|
|
53
|
-
response_schema: DashboardApiResponseSchema,
|
|
54
|
-
handler: async ({ body, adminUser, response }) => {
|
|
55
|
-
if (!ctx.canEditDashboard(adminUser)) {
|
|
56
|
-
response.setStatus(403);
|
|
57
|
-
return { error: 'Dashboard edit is not allowed' };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const dashboard = await ctx.getDashboardRecord(body.slug);
|
|
61
|
-
|
|
62
|
-
if (!dashboard) {
|
|
63
|
-
response.setStatus(404);
|
|
64
|
-
return { error: 'Dashboard not found' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const config = body.config as DashboardConfig;
|
|
68
|
-
|
|
69
|
-
return ctx.persistDashboardConfig(dashboard, config);
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
41
|
}
|
package/endpoint/groups.ts
CHANGED
|
@@ -22,6 +22,10 @@ type GroupEndpointsContext = {
|
|
|
22
22
|
dashboard: DashboardRecord,
|
|
23
23
|
config: DashboardConfig,
|
|
24
24
|
) => Promise<PersistedDashboardResponse>;
|
|
25
|
+
updateDashboardConfig: (
|
|
26
|
+
slug: string,
|
|
27
|
+
mutateConfig: (config: DashboardConfig, dashboard: DashboardRecord) => DashboardConfig | null | Promise<DashboardConfig | null>,
|
|
28
|
+
) => Promise<PersistedDashboardResponse | null>;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export function registerGroupEndpoints(
|
|
@@ -40,26 +44,26 @@ export function registerGroupEndpoints(
|
|
|
40
44
|
return { error: 'Dashboard edit is not allowed' };
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
const
|
|
47
|
+
const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
|
|
48
|
+
const nextOrder = config.groups.length + 1;
|
|
49
|
+
const group: DashboardGroupConfig = {
|
|
50
|
+
id: `group_${randomUUID()}`,
|
|
51
|
+
label: 'New group',
|
|
52
|
+
order: nextOrder,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...config,
|
|
57
|
+
groups: [...config.groups, group],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
44
60
|
|
|
45
|
-
if (!
|
|
61
|
+
if (!updatedDashboard) {
|
|
46
62
|
response.setStatus(404);
|
|
47
63
|
return { error: 'Dashboard not found' };
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
const nextOrder = config.groups.length + 1;
|
|
52
|
-
|
|
53
|
-
const group: DashboardGroupConfig = {
|
|
54
|
-
id: `group_${randomUUID()}`,
|
|
55
|
-
label: 'New group',
|
|
56
|
-
order: nextOrder,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
return ctx.persistDashboardConfig(dashboard, {
|
|
60
|
-
...config,
|
|
61
|
-
groups: [...config.groups, group],
|
|
62
|
-
});
|
|
66
|
+
return updatedDashboard;
|
|
63
67
|
},
|
|
64
68
|
});
|
|
65
69
|
|
|
@@ -76,33 +80,40 @@ export function registerGroupEndpoints(
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
const groupId = body.groupId;
|
|
79
|
-
|
|
83
|
+
let mutationError: string | null = null;
|
|
84
|
+
const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
|
|
85
|
+
const group = config.groups.find((item) => item.id === groupId);
|
|
86
|
+
|
|
87
|
+
if (!group) {
|
|
88
|
+
mutationError = 'Dashboard group not found';
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextGroup: DashboardGroupConfig = {
|
|
93
|
+
...(body.config as EditableDashboardGroupConfig),
|
|
94
|
+
id: group.id,
|
|
95
|
+
order: group.order,
|
|
96
|
+
};
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
return {
|
|
99
|
+
...config,
|
|
100
|
+
groups: config.groups.map((item) => item.id === groupId
|
|
101
|
+
? nextGroup
|
|
102
|
+
: item),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!updatedDashboard) {
|
|
82
107
|
response.setStatus(404);
|
|
83
108
|
return { error: 'Dashboard not found' };
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
const group = config.groups.find((item) => item.id === groupId);
|
|
88
|
-
|
|
89
|
-
if (!group) {
|
|
111
|
+
if (mutationError) {
|
|
90
112
|
response.setStatus(404);
|
|
91
|
-
return { error:
|
|
113
|
+
return { error: mutationError };
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
...(body.config as EditableDashboardGroupConfig),
|
|
96
|
-
id: group.id,
|
|
97
|
-
order: group.order,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return ctx.persistDashboardConfig(dashboard, {
|
|
101
|
-
...config,
|
|
102
|
-
groups: config.groups.map((item) => item.id === groupId
|
|
103
|
-
? nextGroup
|
|
104
|
-
: item),
|
|
105
|
-
});
|
|
116
|
+
return updatedDashboard;
|
|
106
117
|
},
|
|
107
118
|
});
|
|
108
119
|
|
|
@@ -118,42 +129,43 @@ export function registerGroupEndpoints(
|
|
|
118
129
|
return { error: 'Dashboard edit is not allowed' };
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
let mutationError: string | null = null;
|
|
133
|
+
const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
|
|
134
|
+
const sortedGroups = [...config.groups].sort((a, b) => a.order - b.order);
|
|
135
|
+
const currentIndex = sortedGroups.findIndex((group) => group.id === body.groupId);
|
|
122
136
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
if (currentIndex === -1) {
|
|
138
|
+
mutationError = 'Dashboard group not found';
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
const sortedGroups = [...config.groups].sort((a, b) => a.order - b.order);
|
|
130
|
-
const currentIndex = sortedGroups.findIndex((group) => group.id === body.groupId);
|
|
142
|
+
const targetIndex = body.direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
131
143
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
144
|
+
if (targetIndex < 0 || targetIndex >= sortedGroups.length) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
const reorderedGroups = [...sortedGroups];
|
|
149
|
+
const [group] = reorderedGroups.splice(currentIndex, 1);
|
|
150
|
+
reorderedGroups.splice(targetIndex, 0, group);
|
|
138
151
|
|
|
139
|
-
if (targetIndex < 0 || targetIndex >= sortedGroups.length) {
|
|
140
152
|
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
label: dashboard.label,
|
|
144
|
-
revision: dashboard.revision,
|
|
145
|
-
config: ctx.parseStoredDashboardConfig(dashboard.config),
|
|
153
|
+
...config,
|
|
154
|
+
groups: reorderedGroups,
|
|
146
155
|
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!updatedDashboard) {
|
|
159
|
+
response.setStatus(404);
|
|
160
|
+
return { error: 'Dashboard not found' };
|
|
147
161
|
}
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
if (mutationError) {
|
|
164
|
+
response.setStatus(404);
|
|
165
|
+
return { error: mutationError };
|
|
166
|
+
}
|
|
152
167
|
|
|
153
|
-
return
|
|
154
|
-
...config,
|
|
155
|
-
groups: reorderedGroups,
|
|
156
|
-
});
|
|
168
|
+
return updatedDashboard;
|
|
157
169
|
},
|
|
158
170
|
});
|
|
159
171
|
|
|
@@ -170,26 +182,33 @@ export function registerGroupEndpoints(
|
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
const groupId = body.groupId;
|
|
173
|
-
|
|
185
|
+
let mutationError: string | null = null;
|
|
186
|
+
const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
|
|
187
|
+
const nextGroups = config.groups.filter((group) => group.id !== groupId);
|
|
188
|
+
|
|
189
|
+
if (nextGroups.length === config.groups.length) {
|
|
190
|
+
mutationError = 'Dashboard group not found';
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...config,
|
|
196
|
+
groups: nextGroups,
|
|
197
|
+
widgets: config.widgets.filter((widget) => widget.group_id !== groupId),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
174
200
|
|
|
175
|
-
if (!
|
|
201
|
+
if (!updatedDashboard) {
|
|
176
202
|
response.setStatus(404);
|
|
177
203
|
return { error: 'Dashboard not found' };
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
const nextGroups = config.groups.filter((group) => group.id !== groupId);
|
|
182
|
-
|
|
183
|
-
if (nextGroups.length === config.groups.length) {
|
|
206
|
+
if (mutationError) {
|
|
184
207
|
response.setStatus(404);
|
|
185
|
-
return { error:
|
|
208
|
+
return { error: mutationError };
|
|
186
209
|
}
|
|
187
210
|
|
|
188
|
-
return
|
|
189
|
-
...config,
|
|
190
|
-
groups: nextGroups,
|
|
191
|
-
widgets: config.widgets.filter((widget) => widget.group_id !== groupId),
|
|
192
|
-
});
|
|
211
|
+
return updatedDashboard;
|
|
193
212
|
},
|
|
194
213
|
});
|
|
195
214
|
|