@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.
Files changed (58) hide show
  1. package/custom/api/dashboardApi.ts +137 -1
  2. package/custom/model/dashboard.types.ts +32 -22
  3. package/custom/package.json +1 -0
  4. package/custom/pnpm-lock.yaml +31 -0
  5. package/custom/runtime/DashboardRuntime.vue +9 -15
  6. package/custom/runtime/YamlConfigEditor.vue +109 -0
  7. package/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  8. package/custom/widgets/KpiCardWidget.vue +172 -9
  9. package/custom/widgets/chart/ChartWidget.vue +5 -5
  10. package/custom/widgets/registry.ts +4 -4
  11. package/dist/custom/api/dashboardApi.d.ts +46 -1
  12. package/dist/custom/api/dashboardApi.js +90 -0
  13. package/dist/custom/api/dashboardApi.ts +137 -1
  14. package/dist/custom/model/dashboard.types.d.ts +30 -14
  15. package/dist/custom/model/dashboard.types.js +2 -2
  16. package/dist/custom/model/dashboard.types.ts +32 -22
  17. package/dist/custom/package.json +1 -0
  18. package/dist/custom/pnpm-lock.yaml +31 -0
  19. package/dist/custom/queries/useDashboardConfig.d.ts +106 -104
  20. package/dist/custom/queries/useWidgetData.d.ts +106 -104
  21. package/dist/custom/runtime/DashboardRuntime.vue +9 -15
  22. package/dist/custom/runtime/YamlConfigEditor.vue +109 -0
  23. package/dist/custom/skills/adminforth-dashboard/SKILL.md +66 -10
  24. package/dist/custom/widgets/KpiCardWidget.vue +172 -9
  25. package/dist/custom/widgets/chart/ChartWidget.vue +5 -5
  26. package/dist/custom/widgets/registry.js +4 -4
  27. package/dist/custom/widgets/registry.ts +4 -4
  28. package/dist/endpoint/widgets.js +99 -14
  29. package/dist/schema/api.d.ts +11426 -1634
  30. package/dist/schema/api.js +118 -21
  31. package/dist/schema/widget.d.ts +425 -1980
  32. package/dist/schema/widget.js +13 -374
  33. package/dist/schema/widgets/charts.d.ts +1689 -0
  34. package/dist/schema/widgets/charts.js +92 -0
  35. package/dist/schema/widgets/common.d.ts +275 -0
  36. package/dist/schema/widgets/common.js +171 -0
  37. package/dist/schema/widgets/gauge-card.d.ts +172 -0
  38. package/dist/schema/widgets/gauge-card.js +28 -0
  39. package/dist/schema/widgets/kpi-card.d.ts +212 -0
  40. package/dist/schema/widgets/kpi-card.js +43 -0
  41. package/dist/schema/widgets/pivot-table.d.ts +196 -0
  42. package/dist/schema/widgets/pivot-table.js +17 -0
  43. package/dist/schema/widgets/table.d.ts +130 -0
  44. package/dist/schema/widgets/table.js +12 -0
  45. package/dist/services/widgetDataService.js +96 -2
  46. package/endpoint/widgets.ts +173 -26
  47. package/package.json +1 -1
  48. package/schema/api.ts +148 -22
  49. package/schema/widget.ts +43 -425
  50. package/schema/widgets/charts.ts +113 -0
  51. package/schema/widgets/common.ts +194 -0
  52. package/schema/widgets/gauge-card.ts +34 -0
  53. package/schema/widgets/kpi-card.ts +49 -0
  54. package/schema/widgets/pivot-table.ts +24 -0
  55. package/schema/widgets/table.ts +18 -0
  56. package/services/widgetDataService.ts +129 -3
  57. package/shims-vue.d.ts +11 -0
  58. package/tsconfig.json +3 -1
@@ -1,11 +1,15 @@
1
1
  import type {
2
2
  DashboardConfig,
3
3
  EditableDashboardGroupConfig,
4
- EditableDashboardWidgetConfig,
5
4
  DashboardGroupMoveDirection,
5
+ ChartDashboardWidgetConfig,
6
6
  DashboardWidgetConfig,
7
7
  DashboardWidgetConfigValidationError,
8
8
  DashboardWidgetMoveDirection,
9
+ GaugeCardWidgetConfig,
10
+ KpiCardWidgetConfig,
11
+ PivotTableWidgetConfig,
12
+ TableWidgetConfig,
9
13
  } from '../model/dashboard.types.js'
10
14
 
11
15
  export type DashboardResponse = {
@@ -28,6 +32,18 @@ export type DashboardWidgetDataRequest = {
28
32
  }
29
33
  }
30
34
 
35
+ export type ConfigurableTableWidgetConfig = Omit<TableWidgetConfig, 'id' | 'group_id' | 'order'>
36
+ export type ConfigurableKpiCardWidgetConfig = Omit<KpiCardWidgetConfig, 'id' | 'group_id' | 'order'>
37
+ export type ConfigurableGaugeCardWidgetConfig = Omit<GaugeCardWidgetConfig, 'id' | 'group_id' | 'order'>
38
+ export type ConfigurableChartWidgetConfig = Omit<ChartDashboardWidgetConfig, 'id' | 'group_id' | 'order'>
39
+ export type ConfigurableLineChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'line' } }
40
+ export type ConfigurableBarChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'bar' } }
41
+ export type ConfigurableStackedBarChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'stacked_bar' } }
42
+ export type ConfigurablePieChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'pie' } }
43
+ export type ConfigurableHistogramChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'histogram' } }
44
+ export type ConfigurableFunnelChartWidgetConfig = ConfigurableChartWidgetConfig & { chart: { type: 'funnel' } }
45
+ export type ConfigurablePivotTableWidgetConfig = Omit<PivotTableWidgetConfig, 'id' | 'group_id' | 'order'>
46
+
31
47
  export class DashboardApiError extends Error {
32
48
  validationErrors: DashboardWidgetConfigValidationError[]
33
49
 
@@ -205,6 +221,126 @@ export const dashboardApi = {
205
221
  })
206
222
  },
207
223
 
224
+ async configureTableWidget(
225
+ slug: string,
226
+ widgetId: string,
227
+ config: ConfigurableTableWidgetConfig,
228
+ ): Promise<DashboardResponse> {
229
+ return callDashboardApi('/adminapi/v1/dashboard/configure_table_widget', {
230
+ slug,
231
+ widgetId,
232
+ config,
233
+ })
234
+ },
235
+
236
+ async configureKpiCardWidget(
237
+ slug: string,
238
+ widgetId: string,
239
+ config: ConfigurableKpiCardWidgetConfig,
240
+ ): Promise<DashboardResponse> {
241
+ return callDashboardApi('/adminapi/v1/dashboard/configure_kpi_card_widget', {
242
+ slug,
243
+ widgetId,
244
+ config,
245
+ })
246
+ },
247
+
248
+ async configureGaugeCardWidget(
249
+ slug: string,
250
+ widgetId: string,
251
+ config: ConfigurableGaugeCardWidgetConfig,
252
+ ): Promise<DashboardResponse> {
253
+ return callDashboardApi('/adminapi/v1/dashboard/configure_gauge_card_widget', {
254
+ slug,
255
+ widgetId,
256
+ config,
257
+ })
258
+ },
259
+
260
+ async configureLineChartWidget(
261
+ slug: string,
262
+ widgetId: string,
263
+ config: ConfigurableLineChartWidgetConfig,
264
+ ): Promise<DashboardResponse> {
265
+ return callDashboardApi('/adminapi/v1/dashboard/configure_line_chart_widget', {
266
+ slug,
267
+ widgetId,
268
+ config,
269
+ })
270
+ },
271
+
272
+ async configureBarChartWidget(
273
+ slug: string,
274
+ widgetId: string,
275
+ config: ConfigurableBarChartWidgetConfig,
276
+ ): Promise<DashboardResponse> {
277
+ return callDashboardApi('/adminapi/v1/dashboard/configure_bar_chart_widget', {
278
+ slug,
279
+ widgetId,
280
+ config,
281
+ })
282
+ },
283
+
284
+ async configureStackedBarChartWidget(
285
+ slug: string,
286
+ widgetId: string,
287
+ config: ConfigurableStackedBarChartWidgetConfig,
288
+ ): Promise<DashboardResponse> {
289
+ return callDashboardApi('/adminapi/v1/dashboard/configure_stacked_bar_chart_widget', {
290
+ slug,
291
+ widgetId,
292
+ config,
293
+ })
294
+ },
295
+
296
+ async configurePieChartWidget(
297
+ slug: string,
298
+ widgetId: string,
299
+ config: ConfigurablePieChartWidgetConfig,
300
+ ): Promise<DashboardResponse> {
301
+ return callDashboardApi('/adminapi/v1/dashboard/configure_pie_chart_widget', {
302
+ slug,
303
+ widgetId,
304
+ config,
305
+ })
306
+ },
307
+
308
+ async configureHistogramChartWidget(
309
+ slug: string,
310
+ widgetId: string,
311
+ config: ConfigurableHistogramChartWidgetConfig,
312
+ ): Promise<DashboardResponse> {
313
+ return callDashboardApi('/adminapi/v1/dashboard/configure_histogram_chart_widget', {
314
+ slug,
315
+ widgetId,
316
+ config,
317
+ })
318
+ },
319
+
320
+ async configureFunnelChartWidget(
321
+ slug: string,
322
+ widgetId: string,
323
+ config: ConfigurableFunnelChartWidgetConfig,
324
+ ): Promise<DashboardResponse> {
325
+ return callDashboardApi('/adminapi/v1/dashboard/configure_funnel_chart_widget', {
326
+ slug,
327
+ widgetId,
328
+ config,
329
+ })
330
+ },
331
+
332
+ async configurePivotTableWidget(
333
+ slug: string,
334
+ widgetId: string,
335
+ config: ConfigurablePivotTableWidgetConfig,
336
+ ): Promise<DashboardResponse> {
337
+ return callDashboardApi('/adminapi/v1/dashboard/configure_pivot_table_widget', {
338
+ slug,
339
+ widgetId,
340
+ config,
341
+ })
342
+ },
343
+
208
344
  async getDashboardWidgetData(
209
345
  slug: string,
210
346
  widgetId: string,
@@ -36,6 +36,7 @@ export type QueryAggregateOperation = 'sum' | 'count' | 'count_distinct' | 'avg'
36
36
  export type TimeGrain = 'day' | 'week' | 'month' | 'year'
37
37
  export type ValueFormat =
38
38
  | 'number'
39
+ | 'integer'
39
40
  | 'compact_number'
40
41
  | 'currency'
41
42
  | 'percent'
@@ -119,21 +120,17 @@ export type QueryOrderByItem = {
119
120
  export type QueryConfig = {
120
121
  resource: string
121
122
  select?: QuerySelectItem[]
123
+ sparkline?: {
124
+ field: string
125
+ grain: TimeGrain
126
+ as: string
127
+ fill_missing?: Record<string, JsonValue>
128
+ }
122
129
  filters?: FilterExpression
123
130
  group_by?: QueryGroupByItem[]
124
131
  order_by?: QueryOrderByItem[]
125
132
  limit?: number
126
133
  offset?: number
127
- time_series?: {
128
- field: string
129
- grain: TimeGrain
130
- timezone?: string
131
- }
132
- period?: {
133
- field: string
134
- gte?: JsonValue
135
- lt?: JsonValue
136
- }
137
134
  bucket?: {
138
135
  field: string
139
136
  buckets: Array<{ label: string, min?: number, max?: number }>
@@ -178,8 +175,29 @@ export type KpiCardViewConfig = {
178
175
  text?: string
179
176
  field?: string
180
177
  }
181
- comparison?: JsonValue
182
- sparkline?: JsonValue
178
+ comparison?: {
179
+ field: string
180
+ format?: ValueFormat
181
+ positive_is_good?: boolean
182
+ compact?: {
183
+ show?: boolean
184
+ template?: string
185
+ }
186
+ tooltip?: {
187
+ label?: string
188
+ template?: string
189
+ }
190
+ }
191
+ sparkline?: {
192
+ type?: 'line'
193
+ field: string
194
+ x: string
195
+ show_axes?: boolean
196
+ show_labels?: boolean
197
+ fill?: {
198
+ type?: 'gradient' | 'solid'
199
+ }
200
+ }
183
201
  }
184
202
 
185
203
  export type GaugeCardViewConfig = {
@@ -257,14 +275,6 @@ export type DashboardWidgetConfig =
257
275
  | GaugeCardWidgetConfig
258
276
  | PivotTableWidgetConfig
259
277
 
260
- export type EditableDashboardWidgetConfig =
261
- | Omit<EmptyWidgetConfig, 'id' | 'group_id' | 'order'>
262
- | Omit<TableWidgetConfig, 'id' | 'group_id' | 'order'>
263
- | Omit<ChartDashboardWidgetConfig, 'id' | 'group_id' | 'order'>
264
- | Omit<KpiCardWidgetConfig, 'id' | 'group_id' | 'order'>
265
- | Omit<GaugeCardWidgetConfig, 'id' | 'group_id' | 'order'>
266
- | Omit<PivotTableWidgetConfig, 'id' | 'group_id' | 'order'>
267
-
268
278
  export type DashboardWidgetTableData = {
269
279
  kind?: 'table'
270
280
  columns: string[]
@@ -299,10 +309,10 @@ export function serializeDashboardWidgetConfigForEditor(
299
309
  id: _id,
300
310
  group_id: _groupId,
301
311
  order: _order,
302
- ...editableWidget
312
+ ...editableWidgetConfig
303
313
  } = widget
304
314
 
305
- return editableWidget
315
+ return editableWidgetConfig
306
316
  }
307
317
 
308
318
  export function getFieldRefField(value: FieldRef | undefined) {
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
+ "monaco-editor": "^0.55.1",
8
9
  "yaml": "^2.9.0"
9
10
  }
10
11
  }
@@ -8,12 +8,29 @@ importers:
8
8
 
9
9
  .:
10
10
  dependencies:
11
+ monaco-editor:
12
+ specifier: ^0.55.1
13
+ version: 0.55.1
11
14
  yaml:
12
15
  specifier: ^2.9.0
13
16
  version: 2.9.0
14
17
 
15
18
  packages:
16
19
 
20
+ '@types/trusted-types@2.0.7':
21
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
22
+
23
+ dompurify@3.2.7:
24
+ resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
25
+
26
+ marked@14.0.0:
27
+ resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
28
+ engines: {node: '>= 18'}
29
+ hasBin: true
30
+
31
+ monaco-editor@0.55.1:
32
+ resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
33
+
17
34
  yaml@2.9.0:
18
35
  resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
19
36
  engines: {node: '>= 14.6'}
@@ -21,4 +38,18 @@ packages:
21
38
 
22
39
  snapshots:
23
40
 
41
+ '@types/trusted-types@2.0.7':
42
+ optional: true
43
+
44
+ dompurify@3.2.7:
45
+ optionalDependencies:
46
+ '@types/trusted-types': 2.0.7
47
+
48
+ marked@14.0.0: {}
49
+
50
+ monaco-editor@0.55.1:
51
+ dependencies:
52
+ dompurify: 3.2.7
53
+ marked: 14.0.0
54
+
24
55
  yaml@2.9.0: {}
@@ -54,7 +54,7 @@
54
54
  class="fixed inset-0 z-50 flex items-center justify-center bg-black/35 p-4"
55
55
  @click.self="closeGroupConfigEditor"
56
56
  >
57
- <section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
57
+ <section class="w-full max-w-5xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
58
58
  <header class="mb-3 flex items-center justify-between gap-3">
59
59
  <h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
60
60
  Group JSON
@@ -81,12 +81,9 @@
81
81
  </button>
82
82
  </header>
83
83
 
84
- <textarea
84
+ <YamlConfigEditor
85
85
  v-model="groupConfigCode"
86
- class="min-h-[500px] w-full resize-y rounded-lg border border-lightListBorder bg-lightListTable p-3 font-mono text-sm text-lightNavbarText outline-none focus:border-lightPrimaryButtonBackground dark:border-darkListBorder dark:bg-darkListTable dark:text-darkNavbarText dark:focus:border-darkPrimaryButtonBackground"
87
- spellcheck="false"
88
- @keydown.ctrl.enter.prevent="saveGroupConfig"
89
- @keydown.meta.enter.prevent="saveGroupConfig"
86
+ @save="saveGroupConfig"
90
87
  />
91
88
 
92
89
  <div
@@ -120,7 +117,7 @@
120
117
  class="fixed inset-0 z-50 flex items-center justify-center bg-black/35 p-4"
121
118
  @click.self="closeWidgetConfigEditor"
122
119
  >
123
- <section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
120
+ <section class="w-full max-w-5xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
124
121
  <header class="mb-3 flex items-center justify-between gap-3">
125
122
  <h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
126
123
  Widget JSON
@@ -147,12 +144,9 @@
147
144
  </button>
148
145
  </header>
149
146
 
150
- <textarea
147
+ <YamlConfigEditor
151
148
  v-model="widgetConfigCode"
152
- class="min-h-[500px] w-full resize-y rounded-lg border border-lightListBorder bg-lightListTable p-3 font-mono text-sm text-lightNavbarText outline-none focus:border-lightPrimaryButtonBackground dark:border-darkListBorder dark:bg-darkListTable dark:text-darkNavbarText dark:focus:border-darkPrimaryButtonBackground"
153
- spellcheck="false"
154
- @keydown.ctrl.enter.prevent="saveWidgetConfig"
155
- @keydown.meta.enter.prevent="saveWidgetConfig"
149
+ @save="saveWidgetConfig"
156
150
  />
157
151
 
158
152
  <div
@@ -203,12 +197,12 @@ import { computed, ref, watch } from 'vue'
203
197
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
204
198
  import { Button } from '@/afcl'
205
199
  import DashboardGroup from './DashboardGroup.vue'
200
+ import YamlConfigEditor from './YamlConfigEditor.vue'
206
201
  import { DashboardApiError, dashboardApi, type DashboardResponse } from '../api/dashboardApi.js'
207
202
  import type {
208
203
  DashboardConfig,
209
204
  DashboardGroupConfig,
210
205
  EditableDashboardGroupConfig,
211
- EditableDashboardWidgetConfig,
212
206
  DashboardGroupMoveDirection,
213
207
  DashboardWidgetConfig,
214
208
  DashboardWidgetMoveDirection,
@@ -414,13 +408,13 @@ async function saveWidgetConfig() {
414
408
  try {
415
409
  widgetConfigError.value = ''
416
410
  widgetConfigFieldErrors.value = []
417
- const widgetConfig = parseYaml(widgetConfigCode.value) as EditableDashboardWidgetConfig
411
+ const widgetConfig = parseYaml(widgetConfigCode.value) as DashboardWidgetConfig
418
412
 
419
413
  applyDashboardResponse(
420
414
  await dashboardApi.setWidgetConfig(
421
415
  props.dashboardSlug,
422
416
  editingWidgetId.value,
423
- widgetConfig,
417
+ serializeDashboardWidgetConfigForEditor(widgetConfig),
424
418
  ),
425
419
  )
426
420
  closeWidgetConfigEditor()
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div
3
+ ref="editorEl"
4
+ class="min-h-[500px] w-full overflow-hidden rounded-lg border border-lightListBorder bg-lightListTable text-sm dark:border-darkListBorder dark:bg-darkListTable"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import 'monaco-editor/min/vs/editor/editor.main.css'
10
+ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
11
+ import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
12
+ import type * as Monaco from 'monaco-editor'
13
+
14
+ declare global {
15
+ interface Window {
16
+ MonacoEnvironment?: {
17
+ getWorker: () => Worker
18
+ }
19
+ }
20
+ }
21
+
22
+ if (typeof window !== 'undefined' && !window.MonacoEnvironment) {
23
+ window.MonacoEnvironment = {
24
+ getWorker: () => new EditorWorker(),
25
+ }
26
+ }
27
+
28
+ const props = defineProps<{
29
+ modelValue: string
30
+ }>()
31
+
32
+ const emit = defineEmits<{
33
+ 'update:modelValue': [value: string]
34
+ save: []
35
+ }>()
36
+
37
+ const editorEl = ref<HTMLElement | null>(null)
38
+ let monaco: typeof Monaco | null = null
39
+ let editor: Monaco.editor.IStandaloneCodeEditor | null = null
40
+ let resizeObserver: ResizeObserver | null = null
41
+ let applyingExternalValue = false
42
+
43
+ watch(
44
+ () => props.modelValue,
45
+ (value) => {
46
+ if (!editor || value === editor.getValue()) {
47
+ return
48
+ }
49
+
50
+ applyingExternalValue = true
51
+ editor.setValue(value)
52
+ applyingExternalValue = false
53
+ },
54
+ )
55
+
56
+ onMounted(async () => {
57
+ const [loadedMonaco] = await Promise.all([
58
+ import('monaco-editor'),
59
+ import('monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'),
60
+ ])
61
+
62
+ monaco = loadedMonaco
63
+ await nextTick()
64
+
65
+ if (!editorEl.value) {
66
+ return
67
+ }
68
+
69
+ editor = monaco.editor.create(editorEl.value, {
70
+ value: props.modelValue,
71
+ language: 'yaml',
72
+ automaticLayout: true,
73
+ minimap: { enabled: false },
74
+ fontSize: 13,
75
+ lineNumbersMinChars: 3,
76
+ padding: { top: 12, bottom: 12 },
77
+ scrollBeyondLastLine: false,
78
+ tabSize: 2,
79
+ insertSpaces: true,
80
+ wordWrap: 'on',
81
+ wrappingIndent: 'same',
82
+ theme: document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs',
83
+ })
84
+
85
+ editor.onDidChangeModelContent(() => {
86
+ if (!editor || applyingExternalValue) {
87
+ return
88
+ }
89
+
90
+ emit('update:modelValue', editor.getValue())
91
+ })
92
+
93
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
94
+ emit('save')
95
+ })
96
+
97
+ resizeObserver = new ResizeObserver(() => {
98
+ editor?.layout()
99
+ })
100
+ resizeObserver.observe(editorEl.value)
101
+ })
102
+
103
+ onBeforeUnmount(() => {
104
+ resizeObserver?.disconnect()
105
+ resizeObserver = null
106
+ editor?.dispose()
107
+ editor = null
108
+ })
109
+ </script>
@@ -19,7 +19,6 @@ Dashboard root, groups, and widgets are different entities.
19
19
  - Widget tools use widgetId and change target/query/card/chart/table/pivot.
20
20
  - Never call dashboard_set_dashboard_group_config to configure a widget.
21
21
  - Never call dashboard_add_dashboard_group to configure a widget.
22
- - If widget target, label, query, chart, card, table, pivot, variables, formulas, filters, or display fields must change, use dashboard_set_widget_config.
23
22
 
24
23
  ## Tool routing
25
24
 
@@ -27,7 +26,16 @@ Dashboard root, groups, and widgets are different entities.
27
26
  - Add group: dashboard_add_dashboard_group
28
27
  - Rename group: dashboard_set_dashboard_group_config
29
28
  - Add widget slot: dashboard_add_dashboard_widget
30
- - Configure widget: dashboard_set_widget_config
29
+ - Configure table widget: dashboard_configure_table_widget
30
+ - Configure KPI card widget: dashboard_configure_kpi_card_widget
31
+ - Configure gauge card widget: dashboard_configure_gauge_card_widget
32
+ - Configure line chart widget: dashboard_configure_line_chart_widget
33
+ - Configure bar chart widget: dashboard_configure_bar_chart_widget
34
+ - Configure stacked bar chart widget: dashboard_configure_stacked_bar_chart_widget
35
+ - Configure pie chart widget: dashboard_configure_pie_chart_widget
36
+ - Configure histogram chart widget: dashboard_configure_histogram_chart_widget
37
+ - Configure funnel chart widget: dashboard_configure_funnel_chart_widget
38
+ - Configure pivot table widget: dashboard_configure_pivot_table_widget
31
39
  - Move/remove widget/group: matching move/remove tool
32
40
  - Load widget data: dashboard_get_dashboard_widget_data
33
41
 
@@ -42,7 +50,7 @@ Before creating a group, call dashboard_get_config and check existing groups.
42
50
  - If no matching group exists, call dashboard_add_dashboard_group at most once for that requested group.
43
51
  - After dashboard_add_dashboard_group succeeds, extract the new groupId from the returned dashboard response.
44
52
  - If the group needs a label, call dashboard_set_dashboard_group_config once with that groupId.
45
- - After that, the next mutation must be dashboard_add_dashboard_widget or dashboard_set_widget_config, not another dashboard_add_dashboard_group.
53
+ - After that, the next mutation must be dashboard_add_dashboard_widget or a widget configure tool, not another dashboard_add_dashboard_group.
46
54
 
47
55
  Never call dashboard_add_dashboard_group repeatedly for the same user request. If you are about to create a second group for the same label/section, stop and report:
48
56
 
@@ -57,7 +65,7 @@ For any request to create KPI/chart/table/pivot/gauge widgets:
57
65
  3. if needed, rename group once
58
66
  4. for each widget:
59
67
  - dashboard_add_dashboard_widget
60
- - immediately dashboard_set_widget_config for the returned widgetId
68
+ - immediately configure it with the matching dashboard_configure_*_widget tool
61
69
  - confirm target is not empty
62
70
  5. dashboard_get_config
63
71
  6. validate all requested widgets are configured
@@ -79,7 +87,7 @@ This is incomplete for KPI/chart/table/pivot/gauge/spend/revenue/usage widgets.
79
87
 
80
88
  If the same mutation tool is about to be called with the same payload twice, stop and reassess.
81
89
  If dashboard_add_dashboard_group repeats for the same requested group, stop and reuse the groupId from dashboard_get_config.
82
- If dashboard_set_dashboard_group_config repeats while new widgets are target: empty, use dashboard_set_widget_config instead.
90
+ If dashboard_set_dashboard_group_config repeats while new widgets are target: empty, use a widget configure tool instead.
83
91
  After 2 repeated no-op mutations, stop with an explicit error.
84
92
 
85
93
  ## State machine
@@ -90,22 +98,43 @@ dashboard_get_config
90
98
  -> maybe dashboard_add_dashboard_group
91
99
  -> maybe dashboard_set_dashboard_group_config
92
100
  -> dashboard_add_dashboard_widget
93
- -> dashboard_set_widget_config
101
+ -> matching dashboard_configure_*_widget tool
94
102
  -> repeat only the two widget steps for more widgets
95
103
  -> dashboard_get_config
96
104
 
97
105
  Allowed repeats:
98
106
  - dashboard_add_dashboard_widget may repeat once per requested widget.
99
- - dashboard_set_widget_config may repeat once per requested widget.
107
+ - widget configure tools may repeat once per requested widget.
100
108
 
101
109
  Forbidden repeats:
102
110
  - dashboard_add_dashboard_group for the same requested group.
103
111
  - dashboard_set_dashboard_group_config with the same label/groupId.
104
112
 
105
113
  Forbidden substitutions:
106
- - dashboard_set_dashboard_group_config instead of set widget config.
114
+ - dashboard_set_dashboard_group_config instead of a widget configure tool.
107
115
  - dashboard_add_dashboard_group instead of add widget.
108
116
 
117
+ ## Specialized widget tools
118
+
119
+ Available specialized tools:
120
+ - dashboard_configure_table_widget
121
+ - dashboard_configure_kpi_card_widget
122
+ - dashboard_configure_gauge_card_widget
123
+ - dashboard_configure_pivot_table_widget
124
+ - dashboard_configure_line_chart_widget
125
+ - dashboard_configure_bar_chart_widget
126
+ - dashboard_configure_stacked_bar_chart_widget
127
+ - dashboard_configure_pie_chart_widget
128
+ - dashboard_configure_histogram_chart_widget
129
+ - dashboard_configure_funnel_chart_widget
130
+
131
+ Each specialized tool accepts:
132
+ - slug
133
+ - widgetId
134
+ - config
135
+
136
+ The config is the normal widget config for that target without server-owned id, group_id, and order.
137
+
109
138
  ## Widget config keys
110
139
 
111
140
  Use current keys:
@@ -116,22 +145,49 @@ Use table for table.
116
145
  Use pivot for pivot_table.
117
146
 
118
147
  Use target, not type.
148
+ For charts, use target: chart and chart.type for the concrete chart kind.
119
149
  Use query, not dataSource.
120
150
  Use resource, not resourceId.
121
151
 
122
152
  ## Query shape rules
123
153
 
124
- Use query.steps only for funnel charts. Do not use query.steps for kpi_card, gauge_card, table, pivot_table, or normal bar/line/stacked/pie charts.
154
+ Use dashboard_configure_funnel_chart_widget for funnel charts and set query.steps.
155
+ Do not use query.steps for kpi_card, gauge_card, table, pivot_table, line, bar, stacked bar, pie, or histogram charts.
125
156
 
126
157
  For kpi_card and normal charts, use:
127
158
  - query.resource
128
159
  - query.select
129
160
  - optional query.filters
130
161
  - optional query.group_by
131
- - optional query.period
132
162
  - optional query.order_by
133
163
  - optional query.calcs
134
164
 
165
+ ## Date range rules
166
+
167
+ Use only query.filters for time ranges.
168
+ Never use fixed ISO dates for rolling dashboard periods.
169
+ Never use query.period, period.range, query.time_series, or time_series.range.
170
+
171
+ For rolling ranges, use this exact filter shape:
172
+
173
+ filters:
174
+ and:
175
+ - field: created_at
176
+ gte:
177
+ now_minus: 30d
178
+ - field: created_at
179
+ lt:
180
+ now: true
181
+
182
+ Supported relative duration suffixes:
183
+ - h for hours
184
+ - d for days
185
+ - w for weeks
186
+ - mo for months
187
+ - y for years
188
+
189
+ For today/yesterday/last 7 days comparisons, create separate aggregate select items with separate filters and aliases. Do not hard-code calendar dates.
190
+
135
191
  Calculations run after selected fields and aggregates are loaded into a row. Therefore:
136
192
  - aggregate real resource fields first
137
193
  - then calculate derived fields from those aggregate aliases