@adminforth/dashboard 1.3.0 → 1.4.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.
@@ -6,6 +6,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
6
6
  id: string;
7
7
  group_id: string;
8
8
  label?: string | undefined;
9
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
9
10
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
10
11
  width?: number | undefined;
11
12
  height?: number | undefined;
@@ -17,6 +18,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
17
18
  id: string;
18
19
  group_id: string;
19
20
  label?: string | undefined;
21
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
20
22
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
21
23
  width?: number | undefined;
22
24
  height?: number | undefined;
@@ -89,6 +91,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
89
91
  id: string;
90
92
  group_id: string;
91
93
  label?: string | undefined;
94
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
92
95
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
93
96
  width?: number | undefined;
94
97
  height?: number | undefined;
@@ -199,11 +202,16 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
199
202
  };
200
203
  filters?: any;
201
204
  }[];
205
+ calcs?: {
206
+ calc: string;
207
+ as: string;
208
+ }[] | undefined;
202
209
  };
203
210
  } | {
204
211
  id: string;
205
212
  group_id: string;
206
213
  label?: string | undefined;
214
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
207
215
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
208
216
  width?: number | undefined;
209
217
  height?: number | undefined;
@@ -282,6 +290,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
282
290
  id: string;
283
291
  group_id: string;
284
292
  label?: string | undefined;
293
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
285
294
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
286
295
  width?: number | undefined;
287
296
  height?: number | undefined;
@@ -366,6 +375,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
366
375
  id: string;
367
376
  group_id: string;
368
377
  label?: string | undefined;
378
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
369
379
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
370
380
  width?: number | undefined;
371
381
  height?: number | undefined;
@@ -450,6 +460,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
450
460
  id: string;
451
461
  group_id: string;
452
462
  label?: string | undefined;
463
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
453
464
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
454
465
  width?: number | undefined;
455
466
  height?: number | undefined;
@@ -461,6 +472,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
461
472
  id: string;
462
473
  group_id: string;
463
474
  label?: string | undefined;
475
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
464
476
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
465
477
  width?: number | undefined;
466
478
  height?: number | undefined;
@@ -533,6 +545,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
533
545
  id: string;
534
546
  group_id: string;
535
547
  label?: string | undefined;
548
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
536
549
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
537
550
  width?: number | undefined;
538
551
  height?: number | undefined;
@@ -643,11 +656,16 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
643
656
  };
644
657
  filters?: any;
645
658
  }[];
659
+ calcs?: {
660
+ calc: string;
661
+ as: string;
662
+ }[] | undefined;
646
663
  };
647
664
  } | {
648
665
  id: string;
649
666
  group_id: string;
650
667
  label?: string | undefined;
668
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
651
669
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
652
670
  width?: number | undefined;
653
671
  height?: number | undefined;
@@ -726,6 +744,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
726
744
  id: string;
727
745
  group_id: string;
728
746
  label?: string | undefined;
747
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
729
748
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
730
749
  width?: number | undefined;
731
750
  height?: number | undefined;
@@ -810,6 +829,7 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>,
810
829
  id: string;
811
830
  group_id: string;
812
831
  label?: string | undefined;
832
+ variables?: import("../model/dashboard.types.js").DashboardVariables | undefined;
813
833
  size?: import("../model/dashboard.types.js").DashboardWidgetSize | undefined;
814
834
  width?: number | undefined;
815
835
  height?: number | undefined;
@@ -41,6 +41,7 @@ If the user asks how the schema works, how to implement the API, or how to chang
41
41
  Use these tools whenever available:
42
42
 
43
43
  - `dashboard_get_config`
44
+ - `dashboard_set_dashboard_config`
44
45
  - `dashboard_add_dashboard_group`
45
46
  - `dashboard_set_dashboard_group_config`
46
47
  - `dashboard_move_dashboard_group`
@@ -58,6 +59,7 @@ If a dashboard tool is known by name but its argument schema is not loaded, call
58
59
  Do not pass fields between dashboard tools by analogy. Use each tool's schema.
59
60
 
60
61
  - `dashboard_add_dashboard_group` creates a new group. It accepts the dashboard slug only. Never pass `groupId` to this tool.
62
+ - `dashboard_set_dashboard_config` replaces the full dashboard config. Use it when the user explicitly asks to edit the whole dashboard config.
61
63
  - `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
62
64
  - `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
63
65
  - `dashboard_set_widget_config`, `dashboard_move_dashboard_widget`, `dashboard_remove_dashboard_widget`, and `dashboard_get_dashboard_widget_data` operate on an existing widget and need `widgetId`.
@@ -113,6 +115,24 @@ For group requests:
113
115
 
114
116
  If slug is missing, use `default`.
115
117
 
118
+ ## Dashboard Config Workflow
119
+
120
+ Use `dashboard_set_dashboard_config` only when the user explicitly asks to edit the whole dashboard root config.
121
+
122
+ For requests like:
123
+
124
+ - "update root dashboard config"
125
+ - "replace the whole dashboard config"
126
+
127
+ do this:
128
+
129
+ 1. Call `dashboard_get_config`.
130
+ 2. Modify the returned root config, preserving existing `version`, `groups`, and `widgets` unless the user asked to change them.
131
+ 3. Call `dashboard_set_dashboard_config` with the full updated config.
132
+ 4. Return a short summary of the root-level fields changed.
133
+
134
+ Do not use `dashboard_set_dashboard_config` to store reusable widget variables.
135
+
116
136
  ## Widget Config Rules
117
137
 
118
138
  Use the current schema keys exactly:
@@ -124,6 +144,93 @@ Use the current schema keys exactly:
124
144
  - Use `group_by`, not `groupBy`.
125
145
  - Use `order_by`, not `orderBy`.
126
146
  - Use `page_size`, not `pageSize`.
127
- - For funnel charts, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps.
147
+ - For step-based chart queries, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps and add `query.calcs` when derived fields are needed.
128
148
  - Use `card` for KPI and gauge widget view config.
129
149
  - Use `pivot` for pivot table view config.
150
+ - Use `variables` for reusable static maps or constants at widget level.
151
+ - In `query.calcs`, use `lookup($variables.some.map, row_field, default_number)` to read a numeric value from a variable map by the current row/group field.
152
+
153
+ ## Variables And Lookup Calcs
154
+
155
+ Widget config can define variables:
156
+
157
+ ```yaml
158
+ variables:
159
+ token_prices_per_1m:
160
+ input:
161
+ gpt-4.1: 2.00
162
+ gpt-4.1-mini: 0.40
163
+ gpt-4o-mini: 0.15
164
+ output:
165
+ gpt-4.1: 8.00
166
+ gpt-4.1-mini: 1.60
167
+ gpt-4o-mini: 0.60
168
+ cached:
169
+ gpt-4.1: 0.50
170
+ gpt-4.1-mini: 0.10
171
+ gpt-4o-mini: 0.075
172
+ ```
173
+
174
+ Use variables when a calculation needs a static rate table, threshold table, coefficient map, or other reusable constants. In calcs, `lookup($variables.path.to.map, field_name, 0)` returns the value from the map using `field_name` from the current row/group. The third argument is the numeric fallback when the key is missing.
175
+
176
+ Example widget:
177
+
178
+ ```yaml
179
+ target: chart
180
+ label: Model costs
181
+ size: large
182
+ variables:
183
+ token_prices_per_1m:
184
+ input:
185
+ gpt-4.1: 2.00
186
+ gpt-4.1-mini: 0.40
187
+ gpt-4o-mini: 0.15
188
+ output:
189
+ gpt-4.1: 8.00
190
+ gpt-4.1-mini: 1.60
191
+ gpt-4o-mini: 0.60
192
+ cached:
193
+ gpt-4.1: 0.50
194
+ gpt-4.1-mini: 0.10
195
+ gpt-4o-mini: 0.075
196
+
197
+ chart:
198
+ type: stacked_bar
199
+ title: LLM costs by model
200
+ x:
201
+ field: model
202
+ label: Model
203
+ y:
204
+ - field: input_cost
205
+ label: Input
206
+ format: currency
207
+ - field: output_cost
208
+ label: Output
209
+ format: currency
210
+ - field: cached_cost
211
+ label: Cached
212
+ format: currency
213
+
214
+ query:
215
+ resource: model_usage
216
+ select:
217
+ - field: model
218
+ - agg: sum
219
+ field: input_tokens
220
+ as: input_tokens
221
+ - agg: sum
222
+ field: output_tokens
223
+ as: output_tokens
224
+ - agg: sum
225
+ field: cached_tokens
226
+ as: cached_tokens
227
+ group_by:
228
+ - model
229
+ calcs:
230
+ - calc: input_tokens / 1000000 * lookup($variables.token_prices_per_1m.input, model, 0)
231
+ as: input_cost
232
+ - calc: output_tokens / 1000000 * lookup($variables.token_prices_per_1m.output, model, 0)
233
+ as: output_cost
234
+ - calc: cached_tokens / 1000000 * lookup($variables.token_prices_per_1m.cached, model, 0)
235
+ as: cached_cost
236
+ ```
@@ -69,10 +69,10 @@
69
69
 
70
70
  <StackedBarChart
71
71
  v-else-if="chartConfig?.type === 'stacked_bar'"
72
- :rows="rows"
72
+ :rows="stackedBarRows"
73
73
  :x-field="xField"
74
- :y-field="yField"
75
- :series-field="seriesField"
74
+ :y-field="stackedBarYField"
75
+ :series-field="stackedBarSeriesField"
76
76
  :colors="chartConfig.colors"
77
77
  :height="chartHeight"
78
78
  />
@@ -155,6 +155,21 @@ const valueField = computed(() => chartConfig.value?.value?.field || columns.val
155
155
  const pieRows = computed(() => rows.value)
156
156
  const pieLabelField = computed(() => labelField.value)
157
157
  const pieValueField = computed(() => valueField.value)
158
+ const stackedBarYItems = computed(() => {
159
+ const y = chartConfig.value?.y
160
+ return Array.isArray(y) ? y : []
161
+ })
162
+ const stackedBarRows = computed(() => {
163
+ if (chartConfig.value?.type !== 'stacked_bar' || !stackedBarYItems.value.length) {
164
+ return rows.value
165
+ }
166
+
167
+ return rows.value.flatMap((row) => stackedBarYItems.value.map((item) => ({
168
+ [xField.value]: row[xField.value],
169
+ __series: item.label ?? item.field,
170
+ __value: row[item.field],
171
+ })))
172
+ })
158
173
  const barRows = computed(() => {
159
174
  const bucketField = chartConfig.value?.type === 'histogram'
160
175
  ? chartConfig.value.x?.field
@@ -178,6 +193,8 @@ const barRows = computed(() => {
178
193
  const barLabelField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'label' : xField.value)
179
194
  const barValueField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'count' : yField.value)
180
195
  const seriesField = computed(() => chartConfig.value?.series?.field || columns.value[2] || '')
196
+ const stackedBarYField = computed(() => stackedBarYItems.value.length ? '__value' : yField.value)
197
+ const stackedBarSeriesField = computed(() => stackedBarYItems.value.length ? '__series' : seriesField.value)
181
198
  const lineSeriesName = computed(() => {
182
199
  const y = chartConfig.value?.y
183
200
  return Array.isArray(y) ? y[0]?.label : undefined
@@ -1,7 +1,12 @@
1
- import type { IHttpServer } from 'adminforth';
2
- import type { DashboardRecord } from '../services/dashboardConfigService.js';
1
+ import type { AdminUser, IHttpServer } from 'adminforth';
2
+ import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
+ import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
+ import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
3
5
  type DashboardEndpointsContext = {
6
+ canEditDashboard: (adminUser: AdminUser) => boolean;
4
7
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
8
+ persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
9
+ validateDashboardWidgetApiConfig: (widget: DashboardWidgetConfig) => DashboardWidgetConfigValidationError[];
5
10
  };
6
11
  export declare function registerDashboardEndpoints(server: IHttpServer, ctx: DashboardEndpointsContext): void;
7
12
  export {};
@@ -7,8 +7,15 @@ 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 { DashboardApiResponseSchema, SlugRequestSchema } from '../schema/api.js';
10
+ import { normalizeDashboardConfig } from '../custom/model/dashboard.types.js';
11
+ import { DashboardApiResponseSchema, DashboardConfigZodSchema, SetDashboardConfigRequestSchema, SlugRequestSchema, } from '../schema/api.js';
11
12
  import { buildDashboardResponse } from '../services/dashboardConfigService.js';
13
+ function formatDashboardConfigValidationErrors(error) {
14
+ return error.issues.map((issue) => ({
15
+ field: issue.path.length ? issue.path.map(String).join('.') : 'config',
16
+ message: issue.message,
17
+ }));
18
+ }
12
19
  export function registerDashboardEndpoints(server, ctx) {
13
20
  server.endpoint({
14
21
  method: 'POST',
@@ -26,4 +33,41 @@ export function registerDashboardEndpoints(server, ctx) {
26
33
  return buildDashboardResponse(dashboard);
27
34
  }),
28
35
  });
36
+ server.endpoint({
37
+ method: 'POST',
38
+ path: '/dashboard/set_dashboard_config',
39
+ description: 'Replaces one dashboard configuration, including groups and widgets. Superadmin only.',
40
+ request_schema: SetDashboardConfigRequestSchema,
41
+ response_schema: DashboardApiResponseSchema,
42
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
43
+ if (!ctx.canEditDashboard(adminUser)) {
44
+ response.setStatus(403);
45
+ return { error: 'Dashboard edit is not allowed' };
46
+ }
47
+ const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
48
+ const dashboard = yield ctx.getDashboardRecord(slug);
49
+ if (!dashboard) {
50
+ response.setStatus(404);
51
+ return { error: 'Dashboard not found' };
52
+ }
53
+ const normalizedConfig = normalizeDashboardConfig(body === null || body === void 0 ? void 0 : body.config);
54
+ const parsedConfig = DashboardConfigZodSchema.safeParse(normalizedConfig);
55
+ if (!parsedConfig.success) {
56
+ response.setStatus(422);
57
+ return {
58
+ error: 'Invalid dashboard config',
59
+ validationErrors: formatDashboardConfigValidationErrors(parsedConfig.error),
60
+ };
61
+ }
62
+ const widgetValidationErrors = parsedConfig.data.widgets.flatMap((widget, index) => (ctx.validateDashboardWidgetApiConfig(widget).map((error) => (Object.assign(Object.assign({}, error), { field: `widgets.${index}.${error.field}` })))));
63
+ if (widgetValidationErrors.length) {
64
+ response.setStatus(422);
65
+ return {
66
+ error: 'Invalid dashboard config',
67
+ validationErrors: widgetValidationErrors,
68
+ };
69
+ }
70
+ return ctx.persistDashboardConfig(dashboard, parsedConfig.data);
71
+ }),
72
+ });
29
73
  }
@@ -1,5 +1,5 @@
1
1
  import type { AdminUser, IHttpServer } from 'adminforth';
2
- import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
2
+ import type { DashboardConfig, DashboardVariables, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
3
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
4
  import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
5
5
  type WidgetEndpointsContext = {
@@ -14,6 +14,7 @@ type WidgetEndpointsContext = {
14
14
  page: number;
15
15
  pageSize: number;
16
16
  };
17
+ variables?: DashboardVariables;
17
18
  }) => Promise<unknown>;
18
19
  };
19
20
  export declare function registerWidgetEndpoints(server: IHttpServer, ctx: WidgetEndpointsContext): void;
@@ -213,6 +213,7 @@ export function registerWidgetEndpoints(server, ctx) {
213
213
  widget,
214
214
  data: yield ctx.getWidgetData(widget, {
215
215
  pagination: body === null || body === void 0 ? void 0 : body.pagination,
216
+ variables: widget.variables,
216
217
  }),
217
218
  };
218
219
  }),