@adminforth/dashboard 1.0.0 → 1.1.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 (41) hide show
  1. package/README.md +116 -54
  2. package/custom/api/dashboardApi.ts +9 -0
  3. package/custom/model/dashboard.types.ts +158 -1
  4. package/custom/queries/useWidgetData.ts +8 -4
  5. package/custom/runtime/WidgetShell.vue +8 -4
  6. package/custom/widgets/chart/chart.utils.ts +2 -2
  7. package/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
  8. package/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
  9. package/custom/widgets/table/TableWidget.vue +155 -30
  10. package/dist/custom/api/dashboardApi.d.ts +7 -1
  11. package/dist/custom/api/dashboardApi.js +4 -6
  12. package/dist/custom/api/dashboardApi.ts +9 -0
  13. package/dist/custom/model/dashboard.types.d.ts +45 -0
  14. package/dist/custom/model/dashboard.types.js +82 -1
  15. package/dist/custom/model/dashboard.types.ts +158 -1
  16. package/dist/custom/queries/useDashboardConfig.d.ts +42 -0
  17. package/dist/custom/queries/useWidgetData.d.ts +44 -1
  18. package/dist/custom/queries/useWidgetData.js +3 -3
  19. package/dist/custom/queries/useWidgetData.ts +8 -4
  20. package/dist/custom/runtime/WidgetShell.vue +8 -4
  21. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
  22. package/dist/custom/widgets/chart/chart.utils.js +2 -2
  23. package/dist/custom/widgets/chart/chart.utils.ts +2 -2
  24. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
  25. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
  26. package/dist/custom/widgets/table/TableWidget.vue +155 -30
  27. package/dist/endpoint/widgets.d.ts +6 -1
  28. package/dist/endpoint/widgets.js +22 -4
  29. package/dist/schema/api.d.ts +882 -212
  30. package/dist/schema/api.js +11 -2
  31. package/dist/schema/widget.d.ts +542 -4
  32. package/dist/schema/widget.js +111 -1
  33. package/dist/services/widgetConfigValidator.js +32 -6
  34. package/dist/services/widgetDataService.d.ts +8 -6
  35. package/dist/services/widgetDataService.js +133 -11
  36. package/endpoint/widgets.ts +31 -4
  37. package/package.json +1 -1
  38. package/schema/api.ts +11 -1
  39. package/schema/widget.ts +114 -1
  40. package/services/widgetConfigValidator.ts +45 -6
  41. package/services/widgetDataService.ts +201 -19
@@ -23,37 +23,87 @@
23
23
 
24
24
  <div
25
25
  v-else
26
- class="min-h-0 flex-1 overflow-auto"
26
+ class="flex min-h-0 flex-1 flex-col"
27
27
  >
28
- <table class="min-w-max w-full border-collapse text-left text-sm">
29
- <thead class="bg-lightTableHeadingBackground text-xs uppercase text-lightTableHeadingText dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
30
- <tr>
31
- <th
32
- v-for="column in columns"
33
- :key="column"
34
- class="px-3 py-2 font-semibold"
28
+ <div class="min-h-0 flex-1 overflow-auto">
29
+ <table class="min-w-max w-full border-collapse text-left text-sm">
30
+ <thead class="bg-lightTableHeadingBackground text-xs uppercase text-lightTableHeadingText dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
31
+ <tr>
32
+ <th
33
+ v-for="column in columns"
34
+ :key="column"
35
+ class="px-3 py-2 font-semibold"
36
+ >
37
+ {{ column }}
38
+ </th>
39
+ </tr>
40
+ </thead>
41
+
42
+ <tbody>
43
+ <tr
44
+ v-for="(row, index) in tableData.rows"
45
+ :key="`${currentPage}-${index}`"
46
+ class="border-t border-lightListBorder odd:bg-lightTableOddBackground even:bg-lightTableEvenBackground dark:border-darkListBorder odd:dark:bg-darkTableOddBackground even:dark:bg-darkTableEvenBackground"
35
47
  >
36
- {{ column }}
37
- </th>
38
- </tr>
39
- </thead>
40
-
41
- <tbody>
42
- <tr
43
- v-for="(row, index) in tableData.rows"
44
- :key="index"
45
- class="border-t border-lightListBorder odd:bg-lightTableOddBackground even:bg-lightTableEvenBackground dark:border-darkListBorder odd:dark:bg-darkTableOddBackground even:dark:bg-darkTableEvenBackground"
48
+ <td
49
+ v-for="column in columns"
50
+ :key="column"
51
+ class="px-3 py-2 text-lightListTableText dark:text-darkListTableText"
52
+ >
53
+ {{ formatCell(row[column]) }}
54
+ </td>
55
+ </tr>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+
60
+ <div
61
+ v-if="pagination"
62
+ class="flex flex-wrap items-center justify-between gap-2 border-t border-lightListBorder px-3 py-2 text-sm text-lightListTableText dark:border-darkListBorder dark:text-darkListTableText"
63
+ >
64
+ <div>
65
+ {{ pageStart }}-{{ pageEnd }} of {{ pagination.total }}
66
+ </div>
67
+
68
+ <div class="flex items-center gap-2">
69
+ <button
70
+ type="button"
71
+ class="flex h-8 w-8 items-center justify-center rounded border border-lightListBorder text-sm disabled:opacity-45 dark:border-darkListBorder"
72
+ :disabled="currentPage <= 1 || isFetching"
73
+ @click="currentPage -= 1"
74
+ aria-label="Previous page"
46
75
  >
47
- <td
48
- v-for="column in columns"
49
- :key="column"
50
- class="px-3 py-2 text-lightListTableText dark:text-darkListTableText"
76
+ &lt;
77
+ </button>
78
+
79
+ <span class="flex items-center gap-1">
80
+ <span>Page</span>
81
+ <input
82
+ v-model.number="currentPageInput"
83
+ type="number"
84
+ min="1"
85
+ :max="pagination.totalPages"
86
+ class="dashboard-table-page-input h-8 min-w-8 rounded border border-lightListBorder bg-lightTableBackground px-2 text-center text-sm text-lightListTableText dark:border-darkListBorder dark:bg-darkTableBackground dark:text-darkListTableText"
87
+ :style="{ width: `${currentPageInputWidth}ch` }"
88
+ :disabled="isFetching"
89
+ aria-label="Current page"
90
+ @blur="applyCurrentPageInput"
91
+ @keydown.enter="applyCurrentPageInput"
51
92
  >
52
- {{ formatCell(row[column]) }}
53
- </td>
54
- </tr>
55
- </tbody>
56
- </table>
93
+ <span>of {{ pagination.totalPages }}</span>
94
+ </span>
95
+
96
+ <button
97
+ type="button"
98
+ class="flex h-8 w-8 items-center justify-center rounded border border-lightListBorder text-sm disabled:opacity-45 dark:border-darkListBorder"
99
+ :disabled="currentPage >= pagination.totalPages || isFetching"
100
+ @click="currentPage += 1"
101
+ aria-label="Next page"
102
+ >
103
+ &gt;
104
+ </button>
105
+ </div>
106
+ </div>
57
107
  </div>
58
108
  </div>
59
109
  </template>
@@ -61,27 +111,52 @@
61
111
 
62
112
 
63
113
  <script setup lang="ts">
64
- import { computed, watch } from 'vue'
114
+ import { computed, ref, watch } from 'vue'
65
115
  import { useWidgetData } from '../../queries/useWidgetData.js'
66
116
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
67
117
 
118
+ type TableWidgetConfig = {
119
+ columns?: string[]
120
+ pagination?: boolean
121
+ pageSize?: number
122
+ }
123
+
124
+ const DEFAULT_PAGE_SIZE = 10
125
+
68
126
  const props = defineProps<{
69
127
  dashboardSlug: string
70
128
  widget: DashboardWidgetConfig
71
129
  }>()
72
130
 
131
+ const currentPage = ref(1)
132
+ const currentPageInput = ref(1)
133
+ const tableConfig = computed(() => props.widget.table as TableWidgetConfig | undefined)
134
+ const isPaginationEnabled = computed(() => tableConfig.value?.pagination !== false)
135
+ const pageSize = computed(() => tableConfig.value?.pageSize ?? props.widget.query?.limit ?? DEFAULT_PAGE_SIZE)
73
136
  const dashboardSlugRef = computed(() => props.dashboardSlug)
74
137
  const widgetIdRef = computed(() => props.widget.id)
138
+ const widgetDataRequest = computed(() => (
139
+ isPaginationEnabled.value
140
+ ? {
141
+ pagination: {
142
+ page: currentPage.value,
143
+ pageSize: pageSize.value,
144
+ },
145
+ }
146
+ : {}
147
+ ))
75
148
  const {
76
149
  data,
77
150
  isLoading,
151
+ isFetching,
78
152
  error,
79
153
  refetch,
80
- } = useWidgetData(dashboardSlugRef, widgetIdRef)
154
+ } = useWidgetData(dashboardSlugRef, widgetIdRef, widgetDataRequest)
81
155
 
82
156
  watch(
83
157
  () => props.widget,
84
158
  () => {
159
+ currentPage.value = 1
85
160
  void refetch()
86
161
  },
87
162
  { deep: true },
@@ -92,10 +167,47 @@ const tableData = computed(() => {
92
167
  })
93
168
 
94
169
  const columns = computed(() => {
95
- const configuredColumns = (props.widget.table as { columns?: string[] } | undefined)?.columns
170
+ const configuredColumns = tableConfig.value?.columns
96
171
  return configuredColumns ?? tableData.value?.columns ?? []
97
172
  })
98
173
 
174
+ const pagination = computed(() => tableData.value?.pagination)
175
+ const pageStart = computed(() => {
176
+ if (!pagination.value || pagination.value.total === 0) {
177
+ return 0
178
+ }
179
+
180
+ return (pagination.value.page - 1) * pagination.value.pageSize + 1
181
+ })
182
+ const pageEnd = computed(() => {
183
+ if (!pagination.value) {
184
+ return 0
185
+ }
186
+
187
+ return Math.min(pagination.value.page * pagination.value.pageSize, pagination.value.total)
188
+ })
189
+ const currentPageInputWidth = computed(() => {
190
+ const digits = String(currentPageInput.value || currentPage.value).length
191
+ return Math.max(digits + 3, 4)
192
+ })
193
+
194
+ watch(pagination, (nextPagination) => {
195
+ if (nextPagination && currentPage.value > nextPagination.totalPages) {
196
+ currentPage.value = nextPagination.totalPages
197
+ }
198
+ })
199
+
200
+ watch(currentPage, (nextPage) => {
201
+ currentPageInput.value = nextPage
202
+ })
203
+
204
+ function applyCurrentPageInput() {
205
+ const totalPages = pagination.value?.totalPages ?? 1
206
+ const page = Number.isFinite(currentPageInput.value) ? currentPageInput.value : currentPage.value
207
+ currentPage.value = Math.min(Math.max(Math.trunc(page), 1), totalPages)
208
+ currentPageInput.value = currentPage.value
209
+ }
210
+
99
211
  function formatCell(value: unknown) {
100
212
  if (value === null || value === undefined) {
101
213
  return ''
@@ -108,3 +220,16 @@ function formatCell(value: unknown) {
108
220
  return String(value)
109
221
  }
110
222
  </script>
223
+
224
+ <style scoped>
225
+ .dashboard-table-page-input::-webkit-outer-spin-button,
226
+ .dashboard-table-page-input::-webkit-inner-spin-button {
227
+ margin: 0;
228
+ appearance: none;
229
+ }
230
+
231
+ .dashboard-table-page-input {
232
+ appearance: textfield;
233
+ -moz-appearance: textfield;
234
+ }
235
+ </style>
@@ -14,6 +14,12 @@ export type DashboardWidgetDataResponse = {
14
14
  widget: DashboardWidgetConfig;
15
15
  data: unknown;
16
16
  };
17
+ export type DashboardWidgetDataRequest = {
18
+ pagination?: {
19
+ page: number;
20
+ pageSize: number;
21
+ };
22
+ };
17
23
  export declare class DashboardApiError extends Error {
18
24
  validationErrors: DashboardWidgetConfigValidationError[];
19
25
  constructor(message: string, validationErrors?: DashboardWidgetConfigValidationError[]);
@@ -28,5 +34,5 @@ export declare const dashboardApi: {
28
34
  moveDashboardWidget(slug: string, widgetId: string, direction: DashboardWidgetMoveDirection): Promise<DashboardResponse>;
29
35
  removeDashboardWidget(slug: string, widgetId: string): Promise<DashboardResponse>;
30
36
  setWidgetConfig(slug: string, widgetId: string, config: DashboardWidgetConfig): Promise<DashboardResponse>;
31
- getDashboardWidgetData(slug: string, widgetId: string): Promise<DashboardWidgetDataResponse>;
37
+ getDashboardWidgetData(slug: string, widgetId: string, request?: DashboardWidgetDataRequest): Promise<DashboardWidgetDataResponse>;
32
38
  };
@@ -168,12 +168,10 @@ exports.dashboardApi = {
168
168
  });
169
169
  });
170
170
  },
171
- getDashboardWidgetData(slug, widgetId) {
172
- return __awaiter(this, void 0, void 0, function* () {
173
- return callDashboardWidgetDataApi('/adminapi/v1/dashboard/get_dashboard_widget_data', {
174
- slug,
175
- widgetId,
176
- });
171
+ getDashboardWidgetData(slug_1, widgetId_1) {
172
+ return __awaiter(this, arguments, void 0, function* (slug, widgetId, request = {}) {
173
+ return callDashboardWidgetDataApi('/adminapi/v1/dashboard/get_dashboard_widget_data', Object.assign({ slug,
174
+ widgetId }, request));
177
175
  });
178
176
  },
179
177
  };
@@ -24,6 +24,13 @@ export type DashboardWidgetDataResponse = {
24
24
  data: unknown
25
25
  }
26
26
 
27
+ export type DashboardWidgetDataRequest = {
28
+ pagination?: {
29
+ page: number
30
+ pageSize: number
31
+ }
32
+ }
33
+
27
34
  export class DashboardApiError extends Error {
28
35
  validationErrors: DashboardWidgetConfigValidationError[]
29
36
 
@@ -204,10 +211,12 @@ export const dashboardApi = {
204
211
  async getDashboardWidgetData(
205
212
  slug: string,
206
213
  widgetId: string,
214
+ request: DashboardWidgetDataRequest = {},
207
215
  ): Promise<DashboardWidgetDataResponse> {
208
216
  return callDashboardWidgetDataApi('/adminapi/v1/dashboard/get_dashboard_widget_data', {
209
217
  slug,
210
218
  widgetId,
219
+ ...request,
211
220
  })
212
221
  },
213
222
  }
@@ -1,4 +1,33 @@
1
1
  import type { ChartWidgetConfig } from '../widgets/chart/chart.types.js';
2
+ export type AggregationOperation = 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median';
3
+ export type AggregationRule = {
4
+ operation: AggregationOperation;
5
+ field?: string;
6
+ };
7
+ export type GroupByRule = {
8
+ type: 'field';
9
+ field: string;
10
+ } | {
11
+ type: 'date_trunc';
12
+ field: string;
13
+ truncation: 'day' | 'week' | 'month' | 'year';
14
+ timezone?: string;
15
+ };
16
+ export type ResourceWidgetDataSource = {
17
+ type: 'resource';
18
+ resourceId: string;
19
+ columns?: string[];
20
+ sort?: unknown;
21
+ filters?: unknown;
22
+ };
23
+ export type AggregateWidgetDataSource = {
24
+ type: 'aggregate';
25
+ resourceId: string;
26
+ aggregations: Record<string, AggregationRule>;
27
+ groupBy?: GroupByRule;
28
+ filters?: unknown;
29
+ };
30
+ export type WidgetDataSource = ResourceWidgetDataSource | AggregateWidgetDataSource;
2
31
  export type DashboardConfig = {
3
32
  version: number;
4
33
  groups: DashboardGroupConfig[];
@@ -31,6 +60,7 @@ export type DashboardWidgetConfig = {
31
60
  maxWidth?: number | null;
32
61
  order: number;
33
62
  target: DashboardWidgetTarget;
63
+ dataSource?: WidgetDataSource;
34
64
  chart?: ChartWidgetConfig;
35
65
  table?: unknown;
36
66
  kpi_card?: unknown;
@@ -39,7 +69,22 @@ export type DashboardWidgetConfig = {
39
69
  query?: unknown;
40
70
  };
41
71
  export type DashboardWidgetTableData = {
72
+ kind?: 'table';
73
+ columns: string[];
74
+ rows: Record<string, unknown>[];
75
+ pagination?: {
76
+ page: number;
77
+ pageSize: number;
78
+ total: number;
79
+ totalPages: number;
80
+ };
81
+ };
82
+ export type DashboardWidgetAggregateData = {
83
+ kind: 'aggregate';
42
84
  columns: string[];
43
85
  rows: Record<string, unknown>[];
86
+ values?: Record<string, unknown>;
44
87
  };
88
+ export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData;
45
89
  export declare function normalizeDashboardConfig(config: unknown): DashboardConfig;
90
+ export declare function normalizeDashboardWidgetConfig(config: unknown): unknown;
@@ -1,14 +1,95 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeDashboardConfig = normalizeDashboardConfig;
4
+ exports.normalizeDashboardWidgetConfig = normalizeDashboardWidgetConfig;
4
5
  function normalizeDashboardConfig(config) {
5
6
  const value = isRecord(config) ? config : {};
6
7
  return {
7
8
  version: typeof value.version === 'number' ? value.version : 1,
8
9
  groups: Array.isArray(value.groups) ? value.groups : [],
9
- widgets: Array.isArray(value.widgets) ? value.widgets : [],
10
+ widgets: Array.isArray(value.widgets)
11
+ ? value.widgets.map((widget) => normalizeDashboardWidgetConfig(widget))
12
+ : [],
10
13
  };
11
14
  }
15
+ function normalizeDashboardWidgetConfig(config) {
16
+ var _a;
17
+ if (!isRecord(config)) {
18
+ return config;
19
+ }
20
+ const normalized = Object.assign({}, config);
21
+ const target = normalizeDashboardWidgetTarget((_a = normalized.target) !== null && _a !== void 0 ? _a : normalized.type);
22
+ if (target && normalized.target === undefined) {
23
+ normalized.target = target;
24
+ }
25
+ if (target === 'kpi_card') {
26
+ const kpiCardConfig = normalizeKpiCardConfig(normalized);
27
+ if (kpiCardConfig !== undefined) {
28
+ normalized.kpi_card = kpiCardConfig;
29
+ }
30
+ }
31
+ if (target === 'gauge_card') {
32
+ const gaugeCardConfig = normalizeGaugeCardConfig(normalized);
33
+ if (gaugeCardConfig !== undefined) {
34
+ normalized.gauge_card = gaugeCardConfig;
35
+ }
36
+ }
37
+ return normalized;
38
+ }
39
+ function normalizeDashboardWidgetTarget(value) {
40
+ switch (value) {
41
+ case 'empty':
42
+ case 'table':
43
+ case 'chart':
44
+ case 'kpi_card':
45
+ case 'pivot_table':
46
+ case 'gauge_card':
47
+ return value;
48
+ default:
49
+ return undefined;
50
+ }
51
+ }
52
+ function normalizeKpiCardConfig(value) {
53
+ const config = isRecord(value.kpi_card) ? Object.assign({}, value.kpi_card) : {};
54
+ if (typeof value.valueField === 'string' && config.value_field === undefined) {
55
+ config.value_field = value.valueField;
56
+ }
57
+ if (typeof value.labelField === 'string' && config.label_field === undefined) {
58
+ config.label_field = value.labelField;
59
+ }
60
+ if (typeof value.prefix === 'string' && config.prefix === undefined) {
61
+ config.prefix = value.prefix;
62
+ }
63
+ if (typeof value.suffix === 'string' && config.suffix === undefined) {
64
+ config.suffix = value.suffix;
65
+ }
66
+ return Object.keys(config).length ? config : value.kpi_card;
67
+ }
68
+ function normalizeGaugeCardConfig(value) {
69
+ const config = isRecord(value.gauge_card) ? Object.assign({}, value.gauge_card) : {};
70
+ if (typeof value.valueField === 'string' && config.value_field === undefined) {
71
+ config.value_field = value.valueField;
72
+ }
73
+ if (value.min !== undefined && config.min === undefined) {
74
+ config.min = value.min;
75
+ }
76
+ if (value.max !== undefined && config.max === undefined) {
77
+ config.max = value.max;
78
+ }
79
+ if (typeof value.minField === 'string' && config.min_field === undefined) {
80
+ config.min_field = value.minField;
81
+ }
82
+ if (typeof value.maxField === 'string' && config.max_field === undefined) {
83
+ config.max_field = value.maxField;
84
+ }
85
+ if (typeof value.suffix === 'string' && config.suffix === undefined) {
86
+ config.suffix = value.suffix;
87
+ }
88
+ if (typeof value.color === 'string' && config.color === undefined) {
89
+ config.color = value.color;
90
+ }
91
+ return Object.keys(config).length ? config : value.gauge_card;
92
+ }
12
93
  function isRecord(value) {
13
94
  return typeof value === 'object' && value !== null;
14
95
  }
@@ -1,5 +1,42 @@
1
1
  import type { ChartWidgetConfig } from '../widgets/chart/chart.types.js'
2
2
 
3
+ export type AggregationOperation = 'sum' | 'count' | 'avg' | 'min' | 'max' | 'median'
4
+
5
+ export type AggregationRule = {
6
+ operation: AggregationOperation
7
+ field?: string
8
+ }
9
+
10
+ export type GroupByRule =
11
+ | {
12
+ type: 'field'
13
+ field: string
14
+ }
15
+ | {
16
+ type: 'date_trunc'
17
+ field: string
18
+ truncation: 'day' | 'week' | 'month' | 'year'
19
+ timezone?: string
20
+ }
21
+
22
+ export type ResourceWidgetDataSource = {
23
+ type: 'resource'
24
+ resourceId: string
25
+ columns?: string[]
26
+ sort?: unknown
27
+ filters?: unknown
28
+ }
29
+
30
+ export type AggregateWidgetDataSource = {
31
+ type: 'aggregate'
32
+ resourceId: string
33
+ aggregations: Record<string, AggregationRule>
34
+ groupBy?: GroupByRule
35
+ filters?: unknown
36
+ }
37
+
38
+ export type WidgetDataSource = ResourceWidgetDataSource | AggregateWidgetDataSource
39
+
3
40
  export type DashboardConfig = {
4
41
  version: number
5
42
  groups: DashboardGroupConfig[]
@@ -45,6 +82,7 @@ export type DashboardWidgetConfig = {
45
82
  maxWidth?: number | null
46
83
  order: number
47
84
  target: DashboardWidgetTarget
85
+ dataSource?: WidgetDataSource
48
86
  chart?: ChartWidgetConfig
49
87
  table?: unknown
50
88
  kpi_card?: unknown
@@ -54,20 +92,139 @@ export type DashboardWidgetConfig = {
54
92
  }
55
93
 
56
94
  export type DashboardWidgetTableData = {
95
+ kind?: 'table'
57
96
  columns: string[]
58
97
  rows: Record<string, unknown>[]
98
+ pagination?: {
99
+ page: number
100
+ pageSize: number
101
+ total: number
102
+ totalPages: number
103
+ }
59
104
  }
60
105
 
106
+ export type DashboardWidgetAggregateData = {
107
+ kind: 'aggregate'
108
+ columns: string[]
109
+ rows: Record<string, unknown>[]
110
+ values?: Record<string, unknown>
111
+ }
112
+
113
+ export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData
114
+
61
115
  export function normalizeDashboardConfig(config: unknown): DashboardConfig {
62
116
  const value = isRecord(config) ? config : {}
63
117
 
64
118
  return {
65
119
  version: typeof value.version === 'number' ? value.version : 1,
66
120
  groups: Array.isArray(value.groups) ? (value.groups as DashboardGroupConfig[]) : [],
67
- widgets: Array.isArray(value.widgets) ? (value.widgets as DashboardWidgetConfig[]) : [],
121
+ widgets: Array.isArray(value.widgets)
122
+ ? value.widgets.map((widget) => normalizeDashboardWidgetConfig(widget) as DashboardWidgetConfig)
123
+ : [],
68
124
  }
69
125
  }
70
126
 
127
+ export function normalizeDashboardWidgetConfig(config: unknown) {
128
+ if (!isRecord(config)) {
129
+ return config
130
+ }
131
+
132
+ const normalized: Record<string, unknown> = { ...config }
133
+ const target = normalizeDashboardWidgetTarget(normalized.target ?? normalized.type)
134
+
135
+ if (target && normalized.target === undefined) {
136
+ normalized.target = target
137
+ }
138
+
139
+ if (target === 'kpi_card') {
140
+ const kpiCardConfig = normalizeKpiCardConfig(normalized)
141
+
142
+ if (kpiCardConfig !== undefined) {
143
+ normalized.kpi_card = kpiCardConfig
144
+ }
145
+ }
146
+
147
+ if (target === 'gauge_card') {
148
+ const gaugeCardConfig = normalizeGaugeCardConfig(normalized)
149
+
150
+ if (gaugeCardConfig !== undefined) {
151
+ normalized.gauge_card = gaugeCardConfig
152
+ }
153
+ }
154
+
155
+ return normalized
156
+ }
157
+
158
+ function normalizeDashboardWidgetTarget(value: unknown): DashboardWidgetTarget | undefined {
159
+ switch (value) {
160
+ case 'empty':
161
+ case 'table':
162
+ case 'chart':
163
+ case 'kpi_card':
164
+ case 'pivot_table':
165
+ case 'gauge_card':
166
+ return value
167
+ default:
168
+ return undefined
169
+ }
170
+ }
171
+
172
+ function normalizeKpiCardConfig(value: Record<string, unknown>) {
173
+ const config = isRecord(value.kpi_card) ? { ...value.kpi_card } : {}
174
+
175
+ if (typeof value.valueField === 'string' && config.value_field === undefined) {
176
+ config.value_field = value.valueField
177
+ }
178
+
179
+ if (typeof value.labelField === 'string' && config.label_field === undefined) {
180
+ config.label_field = value.labelField
181
+ }
182
+
183
+ if (typeof value.prefix === 'string' && config.prefix === undefined) {
184
+ config.prefix = value.prefix
185
+ }
186
+
187
+ if (typeof value.suffix === 'string' && config.suffix === undefined) {
188
+ config.suffix = value.suffix
189
+ }
190
+
191
+ return Object.keys(config).length ? config : value.kpi_card
192
+ }
193
+
194
+ function normalizeGaugeCardConfig(value: Record<string, unknown>) {
195
+ const config = isRecord(value.gauge_card) ? { ...value.gauge_card } : {}
196
+
197
+ if (typeof value.valueField === 'string' && config.value_field === undefined) {
198
+ config.value_field = value.valueField
199
+ }
200
+
201
+ if (value.min !== undefined && config.min === undefined) {
202
+ config.min = value.min
203
+ }
204
+
205
+ if (value.max !== undefined && config.max === undefined) {
206
+ config.max = value.max
207
+ }
208
+
209
+ if (typeof value.minField === 'string' && config.min_field === undefined) {
210
+ config.min_field = value.minField
211
+ }
212
+
213
+ if (typeof value.maxField === 'string' && config.max_field === undefined) {
214
+ config.max_field = value.maxField
215
+ }
216
+
217
+ if (typeof value.suffix === 'string' && config.suffix === undefined) {
218
+ config.suffix = value.suffix
219
+ }
220
+
221
+ if (typeof value.color === 'string' && config.color === undefined) {
222
+ config.color = value.color
223
+ }
224
+
225
+ return Object.keys(config).length ? config : value.gauge_card
226
+ }
227
+
71
228
  function isRecord(value: unknown): value is Record<string, unknown> {
72
229
  return typeof value === 'object' && value !== null
73
230
  }