@adminforth/dashboard 1.8.0 → 1.10.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 (39) hide show
  1. package/README.md +81 -55
  2. package/custom/api/dashboardApi.ts +73 -36
  3. package/custom/model/dashboard.types.ts +6 -13
  4. package/custom/runtime/DashboardRuntime.vue +26 -22
  5. package/custom/skills/adminforth-dashboard/SKILL.md +13 -20
  6. package/dist/custom/api/dashboardApi.d.ts +24 -18
  7. package/dist/custom/api/dashboardApi.js +42 -18
  8. package/dist/custom/api/dashboardApi.ts +73 -36
  9. package/dist/custom/model/dashboard.types.d.ts +0 -5
  10. package/dist/custom/model/dashboard.types.ts +6 -13
  11. package/dist/custom/queries/useDashboardConfig.d.ts +20 -120
  12. package/dist/custom/queries/useWidgetData.d.ts +20 -120
  13. package/dist/custom/runtime/DashboardRuntime.vue +26 -22
  14. package/dist/custom/skills/adminforth-dashboard/SKILL.md +13 -20
  15. package/dist/endpoint/groups.js +22 -20
  16. package/dist/endpoint/widgets.js +28 -26
  17. package/dist/schema/api.d.ts +230 -3936
  18. package/dist/schema/api.js +7 -12
  19. package/dist/schema/widget.d.ts +20 -200
  20. package/dist/schema/widgets/charts.d.ts +24 -240
  21. package/dist/schema/widgets/common.d.ts +2 -20
  22. package/dist/schema/widgets/common.js +1 -10
  23. package/dist/schema/widgets/gauge-card.d.ts +2 -20
  24. package/dist/schema/widgets/kpi-card.d.ts +2 -20
  25. package/dist/schema/widgets/pivot-table.d.ts +2 -20
  26. package/dist/schema/widgets/table.d.ts +2 -20
  27. package/dist/services/calc-evaluator.d.ts +2 -0
  28. package/dist/services/calc-evaluator.js +54 -0
  29. package/dist/services/dashboardFilterService.d.ts +5 -0
  30. package/dist/services/dashboardFilterService.js +125 -0
  31. package/dist/services/widgetDataService.js +15 -168
  32. package/endpoint/groups.ts +22 -20
  33. package/endpoint/widgets.ts +28 -26
  34. package/package.json +2 -1
  35. package/schema/api.ts +7 -12
  36. package/schema/widgets/common.ts +1 -11
  37. package/services/calc-evaluator.ts +71 -0
  38. package/services/dashboardFilterService.ts +162 -0
  39. package/services/widgetDataService.ts +26 -213
@@ -0,0 +1,54 @@
1
+ import { Parser } from 'expr-eval-fork';
2
+ const LOOKUP_VARIABLE_PATH_RE = /lookup\(\s*\$variables((?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\s*,/g;
3
+ const CALC_PARSER_OPTIONS = {
4
+ allowMemberAccess: false,
5
+ operators: {
6
+ assignment: false,
7
+ concatenate: false,
8
+ conditional: true,
9
+ comparison: true,
10
+ fndef: false,
11
+ in: false,
12
+ logical: true,
13
+ random: false,
14
+ },
15
+ };
16
+ export function evaluateCalc(calc, values, variables = {}) {
17
+ const parser = createCalcParser(variables);
18
+ return parser.parse(normalizeLookupPaths(calc)).evaluate(normalizeCalcValues(values));
19
+ }
20
+ function createCalcParser(variables) {
21
+ const parser = new Parser(CALC_PARSER_OPTIONS);
22
+ parser.functions.lookup = (path, key, defaultValue = 0) => {
23
+ const map = resolveVariablePath(variables, String(path));
24
+ const value = isRecord(map) && Object.prototype.hasOwnProperty.call(map, String(key))
25
+ ? map[String(key)]
26
+ : defaultValue;
27
+ return toFiniteNumber(value);
28
+ };
29
+ return parser;
30
+ }
31
+ function normalizeLookupPaths(calc) {
32
+ return calc.replace(LOOKUP_VARIABLE_PATH_RE, (_match, path) => {
33
+ return `lookup("${path.replace(/^\./, '')}",`;
34
+ });
35
+ }
36
+ function normalizeCalcValues(values) {
37
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [
38
+ key,
39
+ typeof value === 'string' ? value : toFiniteNumber(value),
40
+ ]));
41
+ }
42
+ function toFiniteNumber(value) {
43
+ const numberValue = typeof value === 'number' ? value : Number(value);
44
+ return Number.isFinite(numberValue) ? numberValue : 0;
45
+ }
46
+ function resolveVariablePath(variables, path) {
47
+ return path
48
+ .split('.')
49
+ .filter(Boolean)
50
+ .reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
51
+ }
52
+ function isRecord(value) {
53
+ return typeof value === 'object' && value !== null;
54
+ }
@@ -0,0 +1,5 @@
1
+ import type { IAdminForthAndOrFilter, IAdminForthSingleFilter } from 'adminforth';
2
+ import type { FilterExpression } from '../custom/model/dashboard.types.js';
3
+ export type DashboardQueryFilters = IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>;
4
+ export declare function getAdminForthFilters(filters: FilterExpression | DashboardQueryFilters | undefined): DashboardQueryFilters;
5
+ export declare function mergeFilters(...filters: Array<FilterExpression | DashboardQueryFilters | undefined>): DashboardQueryFilters;
@@ -0,0 +1,125 @@
1
+ import { Filters } from 'adminforth';
2
+ const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
3
+ const FILTER_OPERATORS = {
4
+ eq: Filters.EQ,
5
+ neq: Filters.NEQ,
6
+ gt: Filters.GT,
7
+ gte: Filters.GTE,
8
+ lt: Filters.LT,
9
+ lte: Filters.LTE,
10
+ in: Filters.IN,
11
+ not_in: Filters.NOT_IN,
12
+ like: Filters.LIKE,
13
+ ilike: Filters.ILIKE,
14
+ };
15
+ export function getAdminForthFilters(filters) {
16
+ if (Array.isArray(filters)) {
17
+ return filters.map((filter) => isDashboardFilterExpression(filter)
18
+ ? toAdminForthFilter(filter)
19
+ : filter);
20
+ }
21
+ if (isDashboardFilterExpression(filters)) {
22
+ return toAdminForthFilter(filters);
23
+ }
24
+ if (filters) {
25
+ return filters;
26
+ }
27
+ return [];
28
+ }
29
+ export function mergeFilters(...filters) {
30
+ const merged = [];
31
+ for (const filter of filters) {
32
+ const normalized = getAdminForthFilters(filter);
33
+ if (Array.isArray(normalized)) {
34
+ merged.push(...normalized);
35
+ continue;
36
+ }
37
+ if (normalized) {
38
+ merged.push(normalized);
39
+ }
40
+ }
41
+ if (!merged.length) {
42
+ return [];
43
+ }
44
+ return merged.length === 1 ? merged[0] : merged;
45
+ }
46
+ function isDashboardFilterExpression(value) {
47
+ if (Array.isArray(value)) {
48
+ return true;
49
+ }
50
+ if (!isRecord(value)) {
51
+ return false;
52
+ }
53
+ return 'and' in value
54
+ || 'or' in value
55
+ || 'eq' in value
56
+ || 'neq' in value
57
+ || 'gt' in value
58
+ || 'gte' in value
59
+ || 'lt' in value
60
+ || 'lte' in value
61
+ || 'in' in value
62
+ || 'not_in' in value
63
+ || 'like' in value
64
+ || 'ilike' in value;
65
+ }
66
+ function toAdminForthFilter(filter) {
67
+ if (Array.isArray(filter)) {
68
+ return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
69
+ }
70
+ if ('and' in filter) {
71
+ return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
72
+ }
73
+ if ('or' in filter) {
74
+ return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
75
+ }
76
+ for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
77
+ if (Object.prototype.hasOwnProperty.call(filter, operator)) {
78
+ return createFilter(filter.field, resolveFilterValue(filter[operator]));
79
+ }
80
+ }
81
+ return Filters.AND([]);
82
+ }
83
+ function resolveFilterValue(value) {
84
+ if (Array.isArray(value)) {
85
+ return value.map((item) => resolveFilterValue(item));
86
+ }
87
+ if (!isRecord(value)) {
88
+ return value;
89
+ }
90
+ if (value.now === true) {
91
+ return new Date().toISOString();
92
+ }
93
+ if (typeof value.now_minus === 'string') {
94
+ return subtractDuration(new Date(), value.now_minus).toISOString();
95
+ }
96
+ return value;
97
+ }
98
+ function subtractDuration(now, duration) {
99
+ const match = duration.match(RELATIVE_DURATION_RE);
100
+ if (!match) {
101
+ throw new Error(`Unsupported relative date duration: ${duration}`);
102
+ }
103
+ const amount = Number(match[1]);
104
+ const unit = match[2];
105
+ const date = new Date(now);
106
+ if (unit === 'h') {
107
+ date.setUTCHours(date.getUTCHours() - amount);
108
+ }
109
+ else if (unit === 'd') {
110
+ date.setUTCDate(date.getUTCDate() - amount);
111
+ }
112
+ else if (unit === 'w') {
113
+ date.setUTCDate(date.getUTCDate() - amount * 7);
114
+ }
115
+ else if (unit === 'mo') {
116
+ date.setUTCMonth(date.getUTCMonth() - amount);
117
+ }
118
+ else if (unit === 'y') {
119
+ date.setUTCFullYear(date.getUTCFullYear() - amount);
120
+ }
121
+ return date;
122
+ }
123
+ function isRecord(value) {
124
+ return typeof value === 'object' && value !== null;
125
+ }
@@ -7,24 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { Filters, Sorts } from 'adminforth';
11
- const CALC_IDENTIFIER_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
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
- const VARIABLE_PATH_PREFIX_RE = /^\$variables\.?/;
14
- const SAFE_CALC_EXPRESSION_RE = /^[\d+\-*/().\s?:<>=!]+$/;
15
- const RELATIVE_DURATION_RE = /^(\d+)(h|d|w|mo|y)$/;
16
- const FILTER_OPERATORS = {
17
- eq: Filters.EQ,
18
- neq: Filters.NEQ,
19
- gt: Filters.GT,
20
- gte: Filters.GTE,
21
- lt: Filters.LT,
22
- lte: Filters.LTE,
23
- in: Filters.IN,
24
- not_in: Filters.NOT_IN,
25
- like: Filters.LIKE,
26
- ilike: Filters.ILIKE,
27
- };
10
+ import { Sorts } from 'adminforth';
11
+ import { getAdminForthFilters, mergeFilters, } from './dashboardFilterService.js';
12
+ import { evaluateCalc } from './calc-evaluator.js';
28
13
  export function getWidgetData(adminforth_1, widget_1) {
29
14
  return __awaiter(this, arguments, void 0, function* (adminforth, widget, options = {}) {
30
15
  var _a;
@@ -52,9 +37,9 @@ function getQueryWidgetData(adminforth, query, variables) {
52
37
  if (isStepsQuery(query)) {
53
38
  return getStepsQueryData(adminforth, query, variables);
54
39
  }
55
- const metricSelect = getSingleAggregateMetricSelect(query);
56
- if (metricSelect) {
57
- return getMetricWidgetData(adminforth, query, metricSelect);
40
+ const singleAggregateSelect = getSingleAggregateSelectItem(query);
41
+ if (singleAggregateSelect) {
42
+ return getSingleAggregateWidgetData(adminforth, query, singleAggregateSelect);
58
43
  }
59
44
  const selectedRows = isAggregateQuery(query)
60
45
  ? yield buildAggregateQueryRows(adminforth, query, variables)
@@ -101,18 +86,18 @@ function getStepsQueryData(adminforth, query, variables) {
101
86
  };
102
87
  });
103
88
  }
104
- function getMetricWidgetData(adminforth, query, metric) {
89
+ function getSingleAggregateWidgetData(adminforth, query, aggregate) {
105
90
  return __awaiter(this, void 0, void 0, function* () {
106
91
  var _a;
107
- const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [metric], []);
92
+ const [currentValues = {}] = yield getAggregateRows(adminforth, query.resource, query.filters, [aggregate], []);
108
93
  const values = {
109
- [metric.as]: (_a = currentValues[metric.as]) !== null && _a !== void 0 ? _a : 0,
94
+ [aggregate.as]: (_a = currentValues[aggregate.as]) !== null && _a !== void 0 ? _a : 0,
110
95
  };
111
96
  const rows = query.sparkline
112
- ? yield getMetricSparklineRows(adminforth, query, metric, getAdminForthFilters(query.filters))
97
+ ? yield getSingleAggregateSparklineRows(adminforth, query, aggregate, getAdminForthFilters(query.filters))
113
98
  : [values];
114
99
  const columns = Array.from(new Set([
115
- metric.as,
100
+ aggregate.as,
116
101
  ...(query.sparkline ? [query.sparkline.as] : []),
117
102
  ]));
118
103
  return {
@@ -123,7 +108,7 @@ function getMetricWidgetData(adminforth, query, metric) {
123
108
  };
124
109
  });
125
110
  }
126
- function getMetricSparklineRows(adminforth, query, metric, filters) {
111
+ function getSingleAggregateSparklineRows(adminforth, query, aggregate, filters) {
127
112
  return __awaiter(this, void 0, void 0, function* () {
128
113
  const sparkline = query.sparkline;
129
114
  const groupBy = [{
@@ -131,7 +116,7 @@ function getMetricSparklineRows(adminforth, query, metric, filters) {
131
116
  as: sparkline.as,
132
117
  grain: sparkline.grain,
133
118
  }];
134
- const rows = yield getAggregateRows(adminforth, query.resource, filters, [metric], groupBy);
119
+ const rows = yield getAggregateRows(adminforth, query.resource, filters, [aggregate], groupBy);
135
120
  return rows.map((row) => {
136
121
  var _a;
137
122
  return (Object.assign(Object.assign({}, (_a = query.sparkline) === null || _a === void 0 ? void 0 : _a.fill_missing), row));
@@ -209,29 +194,6 @@ function buildPlainRow(row, select, calcs = [], variables) {
209
194
  }
210
195
  return values;
211
196
  }
212
- function evaluateCalc(calc, values, variables) {
213
- const expression = calc
214
- .replace(LOOKUP_CALL_RE, (_match, path, keyField, defaultValue) => {
215
- var _a;
216
- const map = resolveVariablePath(variables, path);
217
- const key = String((_a = values[keyField]) !== null && _a !== void 0 ? _a : '');
218
- return String(toFiniteNumber(isRecord(map) && Object.prototype.hasOwnProperty.call(map, key)
219
- ? map[key]
220
- : Number(defaultValue)));
221
- })
222
- .replace(CALC_IDENTIFIER_RE, (name) => String(toFiniteNumber(values[name])));
223
- if (!SAFE_CALC_EXPRESSION_RE.test(expression)) {
224
- throw new Error(`Unsupported calc expression: ${calc}`);
225
- }
226
- return Function(`"use strict"; return (${expression});`)();
227
- }
228
- function resolveVariablePath(variables, path) {
229
- return path
230
- .replace(VARIABLE_PATH_PREFIX_RE, '')
231
- .split('.')
232
- .filter(Boolean)
233
- .reduce((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
234
- }
235
197
  function sortRows(rows, orderBy = []) {
236
198
  if (!orderBy.length) {
237
199
  return rows;
@@ -279,7 +241,7 @@ function isAggregateQuery(query) {
279
241
  return Boolean(((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length)
280
242
  || ((_b = query.select) === null || _b === void 0 ? void 0 : _b.some((item) => isAggregateSelectItem(item))));
281
243
  }
282
- function getSingleAggregateMetricSelect(query) {
244
+ function getSingleAggregateSelectItem(query) {
283
245
  var _a, _b;
284
246
  if ((_a = query.group_by) === null || _a === void 0 ? void 0 : _a.length) {
285
247
  return undefined;
@@ -295,7 +257,7 @@ function isStepsQuery(query) {
295
257
  return query.source === 'steps';
296
258
  }
297
259
  function getStepSelect(step) {
298
- return 'select' in step ? step.select : [step.metric];
260
+ return step.select;
299
261
  }
300
262
  function isFieldSelectItem(item) {
301
263
  return 'field' in item && !('agg' in item);
@@ -413,23 +375,6 @@ function getFilterCacheKey(filters) {
413
375
  }
414
376
  return JSON.stringify(filters);
415
377
  }
416
- function mergeFilters(...filters) {
417
- const merged = [];
418
- for (const filter of filters) {
419
- const normalized = getAdminForthFilters(filter);
420
- if (Array.isArray(normalized)) {
421
- merged.push(...normalized);
422
- continue;
423
- }
424
- if (normalized) {
425
- merged.push(normalized);
426
- }
427
- }
428
- if (!merged.length) {
429
- return [];
430
- }
431
- return merged.length === 1 ? merged[0] : merged;
432
- }
433
378
  function getHiddenAggregateAlias(groupBy, select) {
434
379
  const usedAliases = new Set([
435
380
  ...groupBy.map((item) => item.as),
@@ -467,104 +412,6 @@ function formatGroupValue(value, grain) {
467
412
  }
468
413
  return value;
469
414
  }
470
- function getAdminForthFilters(filters) {
471
- if (Array.isArray(filters)) {
472
- return filters.map((filter) => isDashboardFilterExpression(filter)
473
- ? toAdminForthFilter(filter)
474
- : filter);
475
- }
476
- if (isDashboardFilterExpression(filters)) {
477
- return toAdminForthFilter(filters);
478
- }
479
- if (filters) {
480
- return filters;
481
- }
482
- return [];
483
- }
484
- function isDashboardFilterExpression(value) {
485
- if (Array.isArray(value)) {
486
- return true;
487
- }
488
- if (!isRecord(value)) {
489
- return false;
490
- }
491
- return 'and' in value
492
- || 'or' in value
493
- || 'eq' in value
494
- || 'neq' in value
495
- || 'gt' in value
496
- || 'gte' in value
497
- || 'lt' in value
498
- || 'lte' in value
499
- || 'in' in value
500
- || 'not_in' in value
501
- || 'like' in value
502
- || 'ilike' in value;
503
- }
504
- function toAdminForthFilter(filter) {
505
- if (Array.isArray(filter)) {
506
- return Filters.AND(filter.map((item) => toAdminForthFilter(item)));
507
- }
508
- if ('and' in filter) {
509
- return Filters.AND(filter.and.map((item) => toAdminForthFilter(item)));
510
- }
511
- if ('or' in filter) {
512
- return Filters.OR(filter.or.map((item) => toAdminForthFilter(item)));
513
- }
514
- for (const [operator, createFilter] of Object.entries(FILTER_OPERATORS)) {
515
- if (Object.prototype.hasOwnProperty.call(filter, operator)) {
516
- return createFilter(filter.field, resolveFilterValue(filter[operator]));
517
- }
518
- }
519
- return Filters.AND([]);
520
- }
521
- function resolveFilterValue(value) {
522
- if (Array.isArray(value)) {
523
- return value.map((item) => resolveFilterValue(item));
524
- }
525
- if (!isRecord(value)) {
526
- return value;
527
- }
528
- if (value.now === true) {
529
- return new Date().toISOString();
530
- }
531
- if (typeof value.now_minus === 'string') {
532
- return subtractDuration(new Date(), value.now_minus).toISOString();
533
- }
534
- return value;
535
- }
536
- function subtractDuration(now, duration) {
537
- const match = duration.match(RELATIVE_DURATION_RE);
538
- if (!match) {
539
- throw new Error(`Unsupported relative date duration: ${duration}`);
540
- }
541
- const amount = Number(match[1]);
542
- const unit = match[2];
543
- const date = new Date(now);
544
- if (unit === 'h') {
545
- date.setUTCHours(date.getUTCHours() - amount);
546
- }
547
- else if (unit === 'd') {
548
- date.setUTCDate(date.getUTCDate() - amount);
549
- }
550
- else if (unit === 'w') {
551
- date.setUTCDate(date.getUTCDate() - amount * 7);
552
- }
553
- else if (unit === 'mo') {
554
- date.setUTCMonth(date.getUTCMonth() - amount);
555
- }
556
- else if (unit === 'y') {
557
- date.setUTCFullYear(date.getUTCFullYear() - amount);
558
- }
559
- return date;
560
- }
561
- function toFiniteNumber(value) {
562
- const numberValue = typeof value === 'number' ? value : Number(value);
563
- return Number.isFinite(numberValue) ? numberValue : 0;
564
- }
565
- function isRecord(value) {
566
- return typeof value === 'object' && value !== null;
567
- }
568
415
  export function createWidgetDataService(adminforth) {
569
416
  return {
570
417
  getWidgetData: (widget, options) => getWidgetData(adminforth, widget, options),
@@ -6,7 +6,7 @@ import type {
6
6
  DashboardGroupConfig,
7
7
  } from '../custom/model/dashboard.types.js';
8
8
  import {
9
- DashboardApiResponseSchema,
9
+ DashboardMutationResponseSchema,
10
10
  GroupIdRequestSchema,
11
11
  MoveGroupRequestSchema,
12
12
  SetGroupConfigRequestSchema,
@@ -37,13 +37,14 @@ export function registerGroupEndpoints(
37
37
  path: '/dashboard/add_dashboard_group',
38
38
  description: 'Adds a new group to a dashboard configuration. Superadmin only.',
39
39
  request_schema: SlugRequestSchema,
40
- response_schema: DashboardApiResponseSchema,
40
+ response_schema: DashboardMutationResponseSchema,
41
41
  handler: async ({ body, adminUser, response }) => {
42
42
  if (!ctx.canEditDashboard(adminUser)) {
43
43
  response.setStatus(403);
44
- return { error: 'Dashboard edit is not allowed' };
44
+ return { ok: false, error: 'Dashboard edit is not allowed' };
45
45
  }
46
46
 
47
+ let groupId: string | null = null;
47
48
  const updatedDashboard = await ctx.updateDashboardConfig(body.slug, (config) => {
48
49
  const nextOrder = config.groups.length + 1;
49
50
  const group: DashboardGroupConfig = {
@@ -51,6 +52,7 @@ export function registerGroupEndpoints(
51
52
  label: 'New group',
52
53
  order: nextOrder,
53
54
  };
55
+ groupId = group.id;
54
56
 
55
57
  return {
56
58
  ...config,
@@ -60,10 +62,10 @@ export function registerGroupEndpoints(
60
62
 
61
63
  if (!updatedDashboard) {
62
64
  response.setStatus(404);
63
- return { error: 'Dashboard not found' };
65
+ return { ok: false, error: 'Dashboard not found' };
64
66
  }
65
67
 
66
- return updatedDashboard;
68
+ return { ok: true, groupId };
67
69
  },
68
70
  });
69
71
 
@@ -72,11 +74,11 @@ export function registerGroupEndpoints(
72
74
  path: '/dashboard/set_dashboard_group_config',
73
75
  description: 'Replaces editable JSON configuration for a dashboard group while preserving group id and order. Superadmin only.',
74
76
  request_schema: SetGroupConfigRequestSchema,
75
- response_schema: DashboardApiResponseSchema,
77
+ response_schema: DashboardMutationResponseSchema,
76
78
  handler: async ({ body, adminUser, response }) => {
77
79
  if (!ctx.canEditDashboard(adminUser)) {
78
80
  response.setStatus(403);
79
- return { error: 'Dashboard edit is not allowed' };
81
+ return { ok: false, error: 'Dashboard edit is not allowed' };
80
82
  }
81
83
 
82
84
  const groupId = body.groupId;
@@ -105,15 +107,15 @@ export function registerGroupEndpoints(
105
107
 
106
108
  if (!updatedDashboard) {
107
109
  response.setStatus(404);
108
- return { error: 'Dashboard not found' };
110
+ return { ok: false, error: 'Dashboard not found' };
109
111
  }
110
112
 
111
113
  if (mutationError) {
112
114
  response.setStatus(404);
113
- return { error: mutationError };
115
+ return { ok: false, error: mutationError };
114
116
  }
115
117
 
116
- return updatedDashboard;
118
+ return { ok: true };
117
119
  },
118
120
  });
119
121
 
@@ -122,11 +124,11 @@ export function registerGroupEndpoints(
122
124
  path: '/dashboard/move_dashboard_group',
123
125
  description: 'Moves a dashboard group up or down in its dashboard. Superadmin only.',
124
126
  request_schema: MoveGroupRequestSchema,
125
- response_schema: DashboardApiResponseSchema,
127
+ response_schema: DashboardMutationResponseSchema,
126
128
  handler: async ({ body, adminUser, response }) => {
127
129
  if (!ctx.canEditDashboard(adminUser)) {
128
130
  response.setStatus(403);
129
- return { error: 'Dashboard edit is not allowed' };
131
+ return { ok: false, error: 'Dashboard edit is not allowed' };
130
132
  }
131
133
 
132
134
  let mutationError: string | null = null;
@@ -157,15 +159,15 @@ export function registerGroupEndpoints(
157
159
 
158
160
  if (!updatedDashboard) {
159
161
  response.setStatus(404);
160
- return { error: 'Dashboard not found' };
162
+ return { ok: false, error: 'Dashboard not found' };
161
163
  }
162
164
 
163
165
  if (mutationError) {
164
166
  response.setStatus(404);
165
- return { error: mutationError };
167
+ return { ok: false, error: mutationError };
166
168
  }
167
169
 
168
- return updatedDashboard;
170
+ return { ok: true };
169
171
  },
170
172
  });
171
173
 
@@ -174,11 +176,11 @@ export function registerGroupEndpoints(
174
176
  path: '/dashboard/remove_dashboard_group',
175
177
  description: 'Removes a dashboard group and all widgets inside it. Superadmin only.',
176
178
  request_schema: GroupIdRequestSchema,
177
- response_schema: DashboardApiResponseSchema,
179
+ response_schema: DashboardMutationResponseSchema,
178
180
  handler: async ({ body, adminUser, response }) => {
179
181
  if (!ctx.canEditDashboard(adminUser)) {
180
182
  response.setStatus(403);
181
- return { error: 'Dashboard edit is not allowed' };
183
+ return { ok: false, error: 'Dashboard edit is not allowed' };
182
184
  }
183
185
 
184
186
  const groupId = body.groupId;
@@ -200,15 +202,15 @@ export function registerGroupEndpoints(
200
202
 
201
203
  if (!updatedDashboard) {
202
204
  response.setStatus(404);
203
- return { error: 'Dashboard not found' };
205
+ return { ok: false, error: 'Dashboard not found' };
204
206
  }
205
207
 
206
208
  if (mutationError) {
207
209
  response.setStatus(404);
208
- return { error: mutationError };
210
+ return { ok: false, error: mutationError };
209
211
  }
210
212
 
211
- return updatedDashboard;
213
+ return { ok: true };
212
214
  },
213
215
  });
214
216