@adminforth/dashboard 1.0.0 → 1.2.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 (55) hide show
  1. package/README.md +99 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +353 -2
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/DashboardRuntime.vue +2 -1
  6. package/custom/runtime/WidgetRenderer.vue +2 -1
  7. package/custom/runtime/WidgetShell.vue +8 -4
  8. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  9. package/custom/widgets/chart/ChartWidget.vue +45 -12
  10. package/custom/widgets/chart/chart.types.ts +83 -0
  11. package/custom/widgets/chart/chart.utils.ts +2 -2
  12. package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  13. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  14. package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  15. package/custom/widgets/table/TableWidget.vue +155 -30
  16. package/dist/custom/api/dashboardApi.d.ts +7 -1
  17. package/dist/custom/api/dashboardApi.js +4 -6
  18. package/dist/custom/api/dashboardApi.ts +9 -0
  19. package/dist/custom/model/dashboard.types.d.ts +70 -1
  20. package/dist/custom/model/dashboard.types.js +173 -1
  21. package/dist/custom/model/dashboard.types.ts +353 -2
  22. package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
  23. package/dist/custom/queries/useWidgetData.d.ts +44 -3
  24. package/dist/custom/queries/useWidgetData.js +3 -3
  25. package/dist/custom/queries/useWidgetData.ts +8 -4
  26. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  27. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  28. package/dist/custom/runtime/WidgetShell.vue +8 -4
  29. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  30. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  31. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  32. package/dist/custom/widgets/chart/chart.types.js +46 -0
  33. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  34. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  35. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  36. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  37. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
  38. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  39. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
  40. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  41. package/dist/endpoint/widgets.d.ts +6 -1
  42. package/dist/endpoint/widgets.js +41 -6
  43. package/dist/schema/api.d.ts +874 -444
  44. package/dist/schema/api.js +11 -2
  45. package/dist/schema/widget.d.ts +538 -132
  46. package/dist/schema/widget.js +138 -14
  47. package/dist/services/widgetConfigValidator.js +26 -40
  48. package/dist/services/widgetDataService.d.ts +7 -14
  49. package/dist/services/widgetDataService.js +115 -11
  50. package/endpoint/widgets.ts +56 -6
  51. package/package.json +1 -1
  52. package/schema/api.ts +11 -1
  53. package/schema/widget.ts +145 -15
  54. package/services/widgetConfigValidator.ts +36 -44
  55. package/services/widgetDataService.ts +175 -28
@@ -23,6 +23,27 @@ export declare function useDashboardConfig(slug: Ref<string>): {
23
23
  maxWidth?: number | null | undefined;
24
24
  order: number;
25
25
  target: import("../model/dashboard.types.js").DashboardWidgetTarget;
26
+ dataSource?: {
27
+ type: "resource";
28
+ resourceId: string;
29
+ columns?: string[] | undefined;
30
+ sort?: unknown;
31
+ filters?: unknown;
32
+ } | {
33
+ type: "aggregate";
34
+ resourceId: string;
35
+ aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
36
+ groupBy?: {
37
+ type: "field";
38
+ field: string;
39
+ } | {
40
+ type: "date_trunc";
41
+ field: string;
42
+ truncation: "day" | "week" | "month" | "year";
43
+ timezone?: string | undefined;
44
+ } | undefined;
45
+ filters?: unknown;
46
+ } | undefined;
26
47
  chart?: {
27
48
  type: import("../widgets/chart/chart.types.js").ChartWidgetType;
28
49
  title?: string | undefined;
@@ -49,7 +70,6 @@ export declare function useDashboardConfig(slug: Ref<string>): {
49
70
  kpi_card?: unknown;
50
71
  pivot_table?: unknown;
51
72
  gauge_card?: unknown;
52
- query?: unknown;
53
73
  }[];
54
74
  };
55
75
  } | null, import("../api/dashboardApi.js").DashboardResponse | {
@@ -75,6 +95,27 @@ export declare function useDashboardConfig(slug: Ref<string>): {
75
95
  maxWidth?: number | null | undefined;
76
96
  order: number;
77
97
  target: import("../model/dashboard.types.js").DashboardWidgetTarget;
98
+ dataSource?: {
99
+ type: "resource";
100
+ resourceId: string;
101
+ columns?: string[] | undefined;
102
+ sort?: unknown;
103
+ filters?: unknown;
104
+ } | {
105
+ type: "aggregate";
106
+ resourceId: string;
107
+ aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
108
+ groupBy?: {
109
+ type: "field";
110
+ field: string;
111
+ } | {
112
+ type: "date_trunc";
113
+ field: string;
114
+ truncation: "day" | "week" | "month" | "year";
115
+ timezone?: string | undefined;
116
+ } | undefined;
117
+ filters?: unknown;
118
+ } | undefined;
78
119
  chart?: {
79
120
  type: import("../widgets/chart/chart.types.js").ChartWidgetType;
80
121
  title?: string | undefined;
@@ -101,7 +142,6 @@ export declare function useDashboardConfig(slug: Ref<string>): {
101
142
  kpi_card?: unknown;
102
143
  pivot_table?: unknown;
103
144
  gauge_card?: unknown;
104
- query?: unknown;
105
145
  }[];
106
146
  };
107
147
  } | null>;
@@ -1,5 +1,6 @@
1
1
  import { type Ref } from 'vue';
2
- export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>): {
2
+ import { type DashboardWidgetDataRequest } from '../api/dashboardApi.js';
3
+ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>, request?: Ref<DashboardWidgetDataRequest>): {
3
4
  data: Ref<{
4
5
  widget: {
5
6
  id: string;
@@ -12,6 +13,27 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
12
13
  maxWidth?: number | null | undefined;
13
14
  order: number;
14
15
  target: import("../model/dashboard.types.js").DashboardWidgetTarget;
16
+ dataSource?: {
17
+ type: "resource";
18
+ resourceId: string;
19
+ columns?: string[] | undefined;
20
+ sort?: unknown;
21
+ filters?: unknown;
22
+ } | {
23
+ type: "aggregate";
24
+ resourceId: string;
25
+ aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
26
+ groupBy?: {
27
+ type: "field";
28
+ field: string;
29
+ } | {
30
+ type: "date_trunc";
31
+ field: string;
32
+ truncation: "day" | "week" | "month" | "year";
33
+ timezone?: string | undefined;
34
+ } | undefined;
35
+ filters?: unknown;
36
+ } | undefined;
15
37
  chart?: {
16
38
  type: import("../widgets/chart/chart.types.js").ChartWidgetType;
17
39
  title?: string | undefined;
@@ -38,7 +60,6 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
38
60
  kpi_card?: unknown;
39
61
  pivot_table?: unknown;
40
62
  gauge_card?: unknown;
41
- query?: unknown;
42
63
  };
43
64
  data: unknown;
44
65
  } | null, import("../api/dashboardApi.js").DashboardWidgetDataResponse | {
@@ -53,6 +74,27 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
53
74
  maxWidth?: number | null | undefined;
54
75
  order: number;
55
76
  target: import("../model/dashboard.types.js").DashboardWidgetTarget;
77
+ dataSource?: {
78
+ type: "resource";
79
+ resourceId: string;
80
+ columns?: string[] | undefined;
81
+ sort?: unknown;
82
+ filters?: unknown;
83
+ } | {
84
+ type: "aggregate";
85
+ resourceId: string;
86
+ aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
87
+ groupBy?: {
88
+ type: "field";
89
+ field: string;
90
+ } | {
91
+ type: "date_trunc";
92
+ field: string;
93
+ truncation: "day" | "week" | "month" | "year";
94
+ timezone?: string | undefined;
95
+ } | undefined;
96
+ filters?: unknown;
97
+ } | undefined;
56
98
  chart?: {
57
99
  type: import("../widgets/chart/chart.types.js").ChartWidgetType;
58
100
  title?: string | undefined;
@@ -79,7 +121,6 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
79
121
  kpi_card?: unknown;
80
122
  pivot_table?: unknown;
81
123
  gauge_card?: unknown;
82
- query?: unknown;
83
124
  };
84
125
  data: unknown;
85
126
  } | null>;
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useWidgetData = useWidgetData;
13
13
  const vue_1 = require("vue");
14
14
  const dashboardApi_js_1 = require("../api/dashboardApi.js");
15
- function useWidgetData(slug, widgetId) {
15
+ function useWidgetData(slug, widgetId, request) {
16
16
  const data = (0, vue_1.ref)(null);
17
17
  const isLoading = (0, vue_1.ref)(false);
18
18
  const isFetching = (0, vue_1.ref)(false);
@@ -29,7 +29,7 @@ function useWidgetData(slug, widgetId) {
29
29
  isLoading.value = true;
30
30
  }
31
31
  try {
32
- const response = yield dashboardApi_js_1.dashboardApi.getDashboardWidgetData(slug.value, widgetId.value);
32
+ const response = yield dashboardApi_js_1.dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request === null || request === void 0 ? void 0 : request.value);
33
33
  data.value = response;
34
34
  error.value = null;
35
35
  return response;
@@ -44,7 +44,7 @@ function useWidgetData(slug, widgetId) {
44
44
  }
45
45
  });
46
46
  }
47
- (0, vue_1.watch)([slug, widgetId], () => {
47
+ (0, vue_1.watch)(request ? [slug, widgetId, request] : [slug, widgetId], () => {
48
48
  void refetch();
49
49
  }, { immediate: true });
50
50
  return {
@@ -1,7 +1,11 @@
1
1
  import { ref, watch, type Ref } from 'vue'
2
- import { dashboardApi } from '../api/dashboardApi.js'
2
+ import { dashboardApi, type DashboardWidgetDataRequest } from '../api/dashboardApi.js'
3
3
 
4
- export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
4
+ export function useWidgetData(
5
+ slug: Ref<string>,
6
+ widgetId: Ref<string>,
7
+ request?: Ref<DashboardWidgetDataRequest>,
8
+ ) {
5
9
  const data = ref<Awaited<ReturnType<typeof dashboardApi.getDashboardWidgetData>> | null>(null)
6
10
  const isLoading = ref(false)
7
11
  const isFetching = ref(false)
@@ -20,7 +24,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
20
24
  }
21
25
 
22
26
  try {
23
- const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value)
27
+ const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request?.value)
24
28
  data.value = response
25
29
  error.value = null
26
30
  return response
@@ -34,7 +38,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
34
38
  }
35
39
 
36
40
  watch(
37
- [slug, widgetId],
41
+ request ? [slug, widgetId, request] : [slug, widgetId],
38
42
  () => {
39
43
  void refetch()
40
44
  },
@@ -211,6 +211,7 @@ import type {
211
211
  DashboardWidgetConfig,
212
212
  DashboardWidgetMoveDirection,
213
213
  } from '../model/dashboard.types.js'
214
+ import { serializeDashboardWidgetConfigForEditor } from '../model/dashboard.types.js'
214
215
 
215
216
  const props = defineProps<{
216
217
  dashboardSlug: string
@@ -388,7 +389,7 @@ async function removeWidget(widgetId: string) {
388
389
 
389
390
  function editWidget(widget: DashboardWidgetConfig) {
390
391
  editingWidgetId.value = widget.id
391
- widgetConfigCode.value = stringifyYaml(widget)
392
+ widgetConfigCode.value = stringifyYaml(serializeDashboardWidgetConfigForEditor(widget))
392
393
  widgetConfigError.value = ''
393
394
  widgetConfigFieldErrors.value = []
394
395
  }
@@ -26,6 +26,7 @@
26
26
  <script setup lang="ts">
27
27
  import { computed } from 'vue'
28
28
  import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
29
+ import { normalizeChartWidgetConfig } from '../widgets/chart/chart.types.js'
29
30
  import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
30
31
 
31
32
  const props = defineProps<{
@@ -52,7 +53,7 @@ const widgetTitle = computed(() => {
52
53
  }
53
54
 
54
55
  if (props.widget.target === 'chart') {
55
- return props.widget.chart?.title || 'Untitled chart'
56
+ return normalizeChartWidgetConfig(props.widget.chart)?.title || 'Untitled chart'
56
57
  }
57
58
 
58
59
  return getWidgetLabel(props.widget.target)
@@ -135,11 +135,11 @@ const widgetLayoutVars = computed<CSSProperties>(() => {
135
135
  const fixedWidth = formatWidth(props.layout?.width)
136
136
 
137
137
  return {
138
- '--widget-basis': fixedWidth ?? basis,
139
- '--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
138
+ '--widget-basis': clampToContainerWidth(fixedWidth ?? basis),
139
+ '--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis),
140
140
  '--widget-max-width': props.layout?.maxWidth === null
141
- ? 'none'
142
- : fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? 'none',
141
+ ? '100%'
142
+ : clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '100%'),
143
143
  height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
144
144
  }
145
145
  })
@@ -149,4 +149,8 @@ function formatWidth(value: number | undefined) {
149
149
  return `${value}px`
150
150
  }
151
151
  }
152
+
153
+ function clampToContainerWidth(value: string) {
154
+ return `min(${value}, 100%)`
155
+ }
152
156
  </script>
@@ -119,7 +119,7 @@ Use the current schema keys exactly:
119
119
 
120
120
  - Use `target`, not `type`.
121
121
  - Use `label`, not `title`.
122
- - Use `query.resource`, not `resourceId`.
123
- - Use `query.select`, not `columns`.
124
- - Use `query.order`, not `sort`.
125
- - Use `query.limit` for row count.
122
+ - Use `data_source`, not `dataSource`.
123
+ - Use `resource_id`, not `resourceId`.
124
+ - Use `group_by`, not `groupBy`.
125
+ - Use `page_size`, not `pageSize`.
@@ -26,7 +26,7 @@
26
26
  :rows="rows"
27
27
  :x-field="xField"
28
28
  :y-field="yField"
29
- :series-name="chartConfig.series_name"
29
+ :series-name="chartConfig.seriesName"
30
30
  :color="chartConfig.color"
31
31
  :height="chartHeight"
32
32
  />
@@ -91,6 +91,7 @@
91
91
  import { computed, watch } from 'vue'
92
92
  import { useWidgetData } from '../../queries/useWidgetData.js'
93
93
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
94
+ import { normalizeChartWidgetConfig } from './chart.types.js'
94
95
  import BarChart from './bar/BarChart.vue'
95
96
  import FunnelChart from './funnel/FunnelChart.vue'
96
97
  import HistogramChart from './histogram/HistogramChart.vue'
@@ -126,13 +127,45 @@ watch(
126
127
  const chartData = computed(() => data.value?.data as DashboardWidgetTableData | null)
127
128
  const rows = computed(() => chartData.value?.rows ?? [])
128
129
  const columns = computed(() => chartData.value?.columns ?? [])
129
- const chartConfig = computed(() => props.widget.chart)
130
- const xField = computed(() => chartConfig.value?.x_field || columns.value[0])
131
- const yField = computed(() => chartConfig.value?.y_field || columns.value[1])
132
- const labelField = computed(() => chartConfig.value?.label_field || columns.value[0])
133
- const valueField = computed(() => chartConfig.value?.value_field || columns.value[1])
130
+ const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
131
+ const aggregateGroupField = computed(() => {
132
+ const dataSource = props.widget.dataSource
133
+
134
+ if (dataSource?.type !== 'aggregate' || !dataSource.groupBy) {
135
+ return undefined
136
+ }
137
+
138
+ return dataSource.groupBy.field
139
+ })
140
+
141
+ function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
142
+ const resolvedField = field ?? fallbackField
143
+
144
+ if (!resolvedField) {
145
+ return ''
146
+ }
147
+
148
+ if (columns.value.includes(resolvedField)) {
149
+ return resolvedField
150
+ }
151
+
152
+ if (
153
+ aggregateGroupField.value
154
+ && resolvedField === aggregateGroupField.value
155
+ && columns.value.includes('group')
156
+ ) {
157
+ return 'group'
158
+ }
159
+
160
+ return resolvedField
161
+ }
162
+
163
+ const xField = computed(() => resolveChartDimensionField(chartConfig.value?.xField, columns.value[0]))
164
+ const yField = computed(() => chartConfig.value?.yField || columns.value[1])
165
+ const labelField = computed(() => resolveChartDimensionField(chartConfig.value?.labelField, columns.value[0]))
166
+ const valueField = computed(() => chartConfig.value?.valueField || columns.value[1])
134
167
  const pieRows = computed(() => {
135
- if (chartConfig.value?.value_field) {
168
+ if (chartConfig.value?.valueField) {
136
169
  return rows.value
137
170
  }
138
171
 
@@ -147,10 +180,10 @@ const pieRows = computed(() => {
147
180
 
148
181
  return Array.from(groupedRows.values())
149
182
  })
150
- const pieLabelField = computed(() => chartConfig.value?.value_field ? labelField.value : 'label')
151
- const pieValueField = computed(() => chartConfig.value?.value_field ? valueField.value : 'value')
183
+ const pieLabelField = computed(() => chartConfig.value?.valueField ? labelField.value : 'label')
184
+ const pieValueField = computed(() => chartConfig.value?.valueField ? valueField.value : 'value')
152
185
  const barRows = computed(() => {
153
- const bucketField = chartConfig.value?.bucket_field
186
+ const bucketField = chartConfig.value?.bucketField
154
187
 
155
188
  if (!bucketField) {
156
189
  return rows.value
@@ -167,8 +200,8 @@ const barRows = computed(() => {
167
200
  }).length,
168
201
  }))
169
202
  })
170
- const barLabelField = computed(() => chartConfig.value?.bucket_field ? 'label' : labelField.value)
171
- const barValueField = computed(() => chartConfig.value?.bucket_field ? 'count' : valueField.value)
203
+ const barLabelField = computed(() => chartConfig.value?.bucketField ? 'label' : labelField.value)
204
+ const barValueField = computed(() => chartConfig.value?.bucketField ? 'count' : valueField.value)
172
205
  const stackedBarSeries = computed(() => {
173
206
  if (chartConfig.value?.series?.length) {
174
207
  return chartConfig.value.series
@@ -23,3 +23,18 @@ export type ChartWidgetConfig = {
23
23
  color?: string;
24
24
  colors?: string[];
25
25
  };
26
+ export type NormalizedChartWidgetConfig = {
27
+ type: ChartWidgetType;
28
+ title?: string;
29
+ xField?: string;
30
+ yField?: string;
31
+ labelField?: string;
32
+ valueField?: string;
33
+ bucketField?: string;
34
+ buckets?: ChartWidgetBucketConfig[];
35
+ series?: ChartWidgetSeriesConfig[];
36
+ seriesName?: string;
37
+ color?: string;
38
+ colors?: string[];
39
+ };
40
+ export declare function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined;
@@ -1,2 +1,48 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeChartWidgetConfig = normalizeChartWidgetConfig;
4
+ function normalizeChartWidgetConfig(value) {
5
+ const config = asChartWidgetConfigRecord(value);
6
+ if (!config) {
7
+ return undefined;
8
+ }
9
+ const type = normalizeChartWidgetType(config.type);
10
+ if (!type) {
11
+ return undefined;
12
+ }
13
+ const xField = getStringField(config, 'x_field');
14
+ const yField = getStringField(config, 'y_field');
15
+ const labelField = getStringField(config, 'label_field');
16
+ const valueField = getStringField(config, 'value_field');
17
+ const bucketField = getStringField(config, 'bucket_field');
18
+ const seriesName = getStringField(config, 'series_name');
19
+ const title = getStringField(config, 'title');
20
+ const color = getStringField(config, 'color');
21
+ const colors = Array.isArray(config.colors) ? config.colors : undefined;
22
+ const buckets = Array.isArray(config.buckets) ? config.buckets : undefined;
23
+ const series = Array.isArray(config.series) ? config.series : undefined;
24
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type }, (title !== undefined ? { title } : {})), (xField !== undefined ? { xField } : {})), (yField !== undefined ? { yField } : {})), (labelField !== undefined ? { labelField } : {})), (valueField !== undefined ? { valueField } : {})), (bucketField !== undefined ? { bucketField } : {})), (buckets !== undefined ? { buckets } : {})), (series !== undefined ? { series } : {})), (seriesName !== undefined ? { seriesName } : {})), (color !== undefined ? { color } : {})), (colors !== undefined ? { colors } : {}));
25
+ }
26
+ function normalizeChartWidgetType(value) {
27
+ switch (value) {
28
+ case 'line':
29
+ case 'pie':
30
+ case 'bar':
31
+ case 'stacked_bar':
32
+ case 'funnel':
33
+ case 'histogram':
34
+ return value;
35
+ default:
36
+ return undefined;
37
+ }
38
+ }
39
+ function asChartWidgetConfigRecord(value) {
40
+ return isRecord(value) ? value : undefined;
41
+ }
42
+ function getStringField(record, key) {
43
+ const value = record[key];
44
+ return typeof value === 'string' ? value : undefined;
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null;
48
+ }
@@ -32,3 +32,86 @@ export type ChartWidgetConfig = {
32
32
  color?: string
33
33
  colors?: string[]
34
34
  }
35
+
36
+ export type NormalizedChartWidgetConfig = {
37
+ type: ChartWidgetType
38
+ title?: string
39
+ xField?: string
40
+ yField?: string
41
+ labelField?: string
42
+ valueField?: string
43
+ bucketField?: string
44
+ buckets?: ChartWidgetBucketConfig[]
45
+ series?: ChartWidgetSeriesConfig[]
46
+ seriesName?: string
47
+ color?: string
48
+ colors?: string[]
49
+ }
50
+
51
+ export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
52
+ const config = asChartWidgetConfigRecord(value)
53
+
54
+ if (!config) {
55
+ return undefined
56
+ }
57
+
58
+ const type = normalizeChartWidgetType(config.type)
59
+
60
+ if (!type) {
61
+ return undefined
62
+ }
63
+
64
+ const xField = getStringField(config, 'x_field')
65
+ const yField = getStringField(config, 'y_field')
66
+ const labelField = getStringField(config, 'label_field')
67
+ const valueField = getStringField(config, 'value_field')
68
+ const bucketField = getStringField(config, 'bucket_field')
69
+ const seriesName = getStringField(config, 'series_name')
70
+ const title = getStringField(config, 'title')
71
+ const color = getStringField(config, 'color')
72
+ const colors = Array.isArray(config.colors) ? config.colors as string[] : undefined
73
+ const buckets = Array.isArray(config.buckets) ? config.buckets as ChartWidgetBucketConfig[] : undefined
74
+ const series = Array.isArray(config.series) ? config.series as ChartWidgetSeriesConfig[] : undefined
75
+
76
+ return {
77
+ type,
78
+ ...(title !== undefined ? { title } : {}),
79
+ ...(xField !== undefined ? { xField } : {}),
80
+ ...(yField !== undefined ? { yField } : {}),
81
+ ...(labelField !== undefined ? { labelField } : {}),
82
+ ...(valueField !== undefined ? { valueField } : {}),
83
+ ...(bucketField !== undefined ? { bucketField } : {}),
84
+ ...(buckets !== undefined ? { buckets } : {}),
85
+ ...(series !== undefined ? { series } : {}),
86
+ ...(seriesName !== undefined ? { seriesName } : {}),
87
+ ...(color !== undefined ? { color } : {}),
88
+ ...(colors !== undefined ? { colors } : {}),
89
+ }
90
+ }
91
+
92
+ function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
93
+ switch (value) {
94
+ case 'line':
95
+ case 'pie':
96
+ case 'bar':
97
+ case 'stacked_bar':
98
+ case 'funnel':
99
+ case 'histogram':
100
+ return value
101
+ default:
102
+ return undefined
103
+ }
104
+ }
105
+
106
+ function asChartWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
107
+ return isRecord(value) ? value : undefined
108
+ }
109
+
110
+ function getStringField(record: Record<string, unknown>, key: string) {
111
+ const value = record[key]
112
+ return typeof value === 'string' ? value : undefined
113
+ }
114
+
115
+ function isRecord(value: unknown): value is Record<string, unknown> {
116
+ return typeof value === 'object' && value !== null
117
+ }
@@ -1,5 +1,5 @@
1
1
  export declare const CHART_COLORS: string[];
2
2
  export declare function toFiniteNumber(value: unknown): number;
3
- export declare function formatChartValue(value: number): string;
3
+ export declare function formatChartValue(value: number, options?: Intl.NumberFormatOptions): string;
4
4
  export declare function formatChartLabel(value: unknown): string;
5
5
  export declare function formatChartAxisLabel(value: unknown, maxLength?: number): string;
@@ -19,8 +19,8 @@ function toFiniteNumber(value) {
19
19
  const numberValue = typeof value === 'number' ? value : Number(value);
20
20
  return Number.isFinite(numberValue) ? numberValue : 0;
21
21
  }
22
- function formatChartValue(value) {
23
- return new Intl.NumberFormat().format(value);
22
+ function formatChartValue(value, options = {}) {
23
+ return new Intl.NumberFormat(undefined, options).format(value);
24
24
  }
25
25
  function formatChartLabel(value) {
26
26
  if (typeof value !== 'string') {
@@ -14,8 +14,8 @@ export function toFiniteNumber(value: unknown) {
14
14
  return Number.isFinite(numberValue) ? numberValue : 0
15
15
  }
16
16
 
17
- export function formatChartValue(value: number) {
18
- return new Intl.NumberFormat().format(value)
17
+ export function formatChartValue(value: number, options: Intl.NumberFormatOptions = {}) {
18
+ return new Intl.NumberFormat(undefined, options).format(value)
19
19
  }
20
20
 
21
21
  export function formatChartLabel(value: unknown) {