@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
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
+ import {
5
+ normalizeGaugeCardWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
4
7
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
8
  import { CHART_COLORS, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
@@ -18,6 +21,32 @@ const {
18
21
  refetch,
19
22
  } = useWidgetData(dashboardSlugRef, widgetIdRef)
20
23
 
24
+ function parseOptionalNumber(value: unknown): number | undefined {
25
+ if (value === null || value === undefined || value === '') {
26
+ return undefined
27
+ }
28
+
29
+ const parsed = typeof value === 'number' ? value : Number(value)
30
+ return Number.isFinite(parsed) ? parsed : undefined
31
+ }
32
+
33
+ function countFractionDigits(value: number) {
34
+ if (!Number.isFinite(value)) {
35
+ return 0
36
+ }
37
+
38
+ const normalizedValue = value.toString().toLowerCase()
39
+ const [coefficient, exponentValue] = normalizedValue.split('e')
40
+ const exponent = exponentValue ? Number(exponentValue) : 0
41
+ const decimalDigits = coefficient.split('.')[1]?.length ?? 0
42
+
43
+ return Math.max(decimalDigits - exponent, 0)
44
+ }
45
+
46
+ function normalizeDisplayValue(value: number, useWholeNumbers: boolean) {
47
+ return useWholeNumbers ? Math.trunc(value) : value
48
+ }
49
+
21
50
  watch(
22
51
  () => props.widget,
23
52
  () => {
@@ -26,20 +55,42 @@ watch(
26
55
  { deep: true },
27
56
  )
28
57
 
29
- const gaugeConfig = computed(() => props.widget.gauge_card as {
30
- value_field?: string
31
- min?: number
32
- max?: number
33
- suffix?: string
34
- color?: string
35
- } | undefined)
58
+ const gaugeConfig = computed(() => normalizeGaugeCardWidgetConfig(props.widget.gauge_card))
36
59
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
37
60
  const columns = computed(() => widgetData.value?.columns ?? [])
38
61
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
39
- const valueField = computed(() => gaugeConfig.value?.value_field || columns.value[0])
40
- const minValue = computed(() => gaugeConfig.value?.min ?? 0)
41
- const maxValue = computed(() => gaugeConfig.value?.max ?? 100)
62
+ const valueField = computed(() => gaugeConfig.value?.valueField || columns.value[0])
63
+ const minField = computed(() => gaugeConfig.value?.minField)
64
+ const maxField = computed(() => gaugeConfig.value?.maxField)
65
+ const minValue = computed(() => {
66
+ const dynamicMin = minField.value ? parseOptionalNumber(firstRow.value[minField.value]) : undefined
67
+ return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
68
+ })
69
+ const maxValue = computed(() => {
70
+ const dynamicMax = maxField.value ? parseOptionalNumber(firstRow.value[maxField.value]) : undefined
71
+ return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.max) ?? 100
72
+ })
42
73
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
74
+ const fractionDigits = computed(() => Math.min([
75
+ value.value,
76
+ minValue.value,
77
+ maxValue.value,
78
+ ].reduce((maxDigits, currentValue) => Math.max(maxDigits, countFractionDigits(currentValue)), 0), 3))
79
+ const shouldUseWholeNumbers = computed(() => Math.abs(maxValue.value) >= 1000)
80
+ const formattedValue = computed(() => formatChartValue(normalizeDisplayValue(value.value, shouldUseWholeNumbers.value), {
81
+ minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
82
+ maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
83
+ }))
84
+ const formattedMinValue = computed(() => formatChartValue(normalizeDisplayValue(minValue.value, shouldUseWholeNumbers.value), {
85
+ minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
86
+ maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
87
+ }))
88
+ const formattedMaxValue = computed(() => {
89
+ return formatChartValue(normalizeDisplayValue(maxValue.value, shouldUseWholeNumbers.value), {
90
+ minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
91
+ maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
92
+ })
93
+ })
43
94
  const progress = computed(() => {
44
95
  const range = maxValue.value - minValue.value
45
96
  return range > 0 ? Math.min(Math.max((value.value - minValue.value) / range, 0), 1) : 0
@@ -97,10 +148,10 @@ const gaugeColor = computed(() => gaugeConfig.value?.color || CHART_COLORS[0])
97
148
  </svg>
98
149
 
99
150
  <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
100
- {{ formatChartValue(value) }}{{ gaugeConfig?.suffix ?? '' }}
151
+ {{ formattedValue }}{{ gaugeConfig?.suffix ?? '' }}
101
152
  </div>
102
153
  <div class="text-sm text-lightListTableText dark:text-darkListTableText">
103
- {{ formatChartValue(minValue) }} - {{ formatChartValue(maxValue) }}
154
+ {{ formattedMinValue }} - {{ formattedMaxValue }}
104
155
  </div>
105
156
  </div>
106
157
  </div>
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
+ import {
5
+ normalizeKpiCardWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
4
7
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
8
  import { formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
@@ -26,17 +29,12 @@ watch(
26
29
  { deep: true },
27
30
  )
28
31
 
29
- const kpiConfig = computed(() => props.widget.kpi_card as {
30
- value_field?: string
31
- label_field?: string
32
- prefix?: string
33
- suffix?: string
34
- } | undefined)
32
+ const kpiConfig = computed(() => normalizeKpiCardWidgetConfig(props.widget.kpi_card))
35
33
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
36
34
  const columns = computed(() => widgetData.value?.columns ?? [])
37
35
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
38
- const valueField = computed(() => kpiConfig.value?.value_field || columns.value[0])
39
- const labelField = computed(() => kpiConfig.value?.label_field)
36
+ const valueField = computed(() => kpiConfig.value?.valueField || columns.value[0])
37
+ const labelField = computed(() => kpiConfig.value?.labelField)
40
38
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
41
39
  const label = computed(() => labelField.value ? String(firstRow.value[labelField.value]) : props.widget.label)
42
40
  const formattedValue = computed(() => `${kpiConfig.value?.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.suffix ?? ''}`)
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
- import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
4
+ import {
5
+ normalizePivotTableWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
7
+ import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
5
8
  import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
7
10
  const props = defineProps<{
@@ -26,21 +29,38 @@ watch(
26
29
  { deep: true },
27
30
  )
28
31
 
29
- const pivotConfig = computed(() => props.widget.pivot_table as {
30
- row_field?: string
31
- column_field?: string
32
- value_field?: string
33
- aggregation?: 'count' | 'sum'
34
- } | undefined)
35
- const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
32
+ const pivotConfig = computed(() => normalizePivotTableWidgetConfig(props.widget.pivot_table))
33
+ const widgetData = computed(() => data.value?.data as DashboardWidgetData | null)
36
34
  const rows = computed(() => widgetData.value?.rows ?? [])
37
35
  const columns = computed(() => widgetData.value?.columns ?? [])
38
- const rowField = computed(() => pivotConfig.value?.row_field || columns.value[0])
39
- const columnField = computed(() => pivotConfig.value?.column_field || columns.value[1])
40
- const valueField = computed(() => pivotConfig.value?.value_field || columns.value[2])
36
+ const isAggregateData = computed(() => widgetData.value?.kind === 'aggregate')
37
+ const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.columnField)
38
+ const rowField = computed(() => pivotConfig.value?.rowField || (isAggregateData.value ? 'group' : columns.value[0]))
39
+ const columnField = computed(() => pivotConfig.value?.columnField || columns.value[1])
40
+ const valueField = computed(() => pivotConfig.value?.valueField || columns.value[2] || columns.value[1])
41
41
  const aggregation = computed(() => pivotConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
42
- const pivotColumnLabels = computed(() => Array.from(new Set(rows.value.map((row) => formatChartLabel(row[columnField.value])))))
42
+ const pivotColumnLabels = computed(() => {
43
+ if (shouldRenderAggregateMatrix.value) {
44
+ return columns.value.filter((column) => column !== rowField.value)
45
+ }
46
+
47
+ return Array.from(new Set(rows.value.map((row) => formatChartLabel(row[columnField.value]))))
48
+ })
43
49
  const pivotRows = computed(() => {
50
+ if (shouldRenderAggregateMatrix.value) {
51
+ return rows.value.map((row) => {
52
+ const item: Record<string, number | string> = {
53
+ label: formatChartLabel(row[rowField.value]),
54
+ }
55
+
56
+ for (const column of pivotColumnLabels.value) {
57
+ item[column] = toFiniteNumber(row[column])
58
+ }
59
+
60
+ return item
61
+ })
62
+ }
63
+
44
64
  const rowMap = new Map<string, Record<string, number | string>>()
45
65
 
46
66
  for (const row of rows.value) {
@@ -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 ?? 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>
@@ -9,7 +9,12 @@ type WidgetEndpointsContext = {
9
9
  persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
10
10
  buildDashboardResponse: (dashboard: DashboardRecord) => PersistedDashboardResponse;
11
11
  validateDashboardWidgetApiConfig: (widget: DashboardWidgetConfig) => DashboardWidgetConfigValidationError[];
12
- getWidgetData: (widget: DashboardWidgetConfig) => Promise<unknown>;
12
+ getWidgetData: (widget: DashboardWidgetConfig, options?: {
13
+ pagination?: {
14
+ page: number;
15
+ pageSize: number;
16
+ };
17
+ }) => Promise<unknown>;
13
18
  };
14
19
  export declare function registerWidgetEndpoints(server: IHttpServer, ctx: WidgetEndpointsContext): void;
15
20
  export {};
@@ -8,7 +8,32 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { randomUUID } from 'crypto';
11
- import { DashboardApiResponseSchema, DashboardWidgetDataResponseSchema, GroupIdRequestSchema, MoveWidgetRequestSchema, SetWidgetConfigRequestSchema, WidgetIdRequestSchema, } from '../schema/api.js';
11
+ import { normalizeDashboardWidgetConfig, } from '../custom/model/dashboard.types.js';
12
+ import { DashboardApiResponseSchema, DashboardWidgetDataResponseSchema, GroupIdRequestSchema, MoveWidgetRequestSchema, SetWidgetConfigRequestSchema, WidgetDataRequestSchema, WidgetIdRequestSchema, } from '../schema/api.js';
13
+ import { StoredWidgetConfigSchema } from '../schema/widget.js';
14
+ function formatWidgetConfigValidationErrors(error) {
15
+ return error.issues.map((issue) => ({
16
+ field: issue.path.length ? formatWidgetConfigFieldPath(issue.path.map(String).join('.')) : 'config',
17
+ message: issue.message,
18
+ }));
19
+ }
20
+ function formatWidgetConfigApiValidationErrors(errors) {
21
+ return errors.map((error) => (Object.assign(Object.assign({}, error), { field: formatWidgetConfigFieldPath(error.field) })));
22
+ }
23
+ function formatWidgetConfigFieldPath(field) {
24
+ const fieldAliases = new Map([
25
+ ['minWidth', 'min_width'],
26
+ ['maxWidth', 'max_width'],
27
+ ['dataSource', 'data_source'],
28
+ ['resourceId', 'resource_id'],
29
+ ['groupBy', 'group_by'],
30
+ ['pageSize', 'page_size'],
31
+ ]);
32
+ return field
33
+ .split('.')
34
+ .map((segment) => { var _a; return (_a = fieldAliases.get(segment)) !== null && _a !== void 0 ? _a : segment; })
35
+ .join('.');
36
+ }
12
37
  export function registerWidgetEndpoints(server, ctx) {
13
38
  server.endpoint({
14
39
  method: 'POST',
@@ -140,13 +165,21 @@ export function registerWidgetEndpoints(server, ctx) {
140
165
  response.setStatus(404);
141
166
  return { error: 'Dashboard widget not found' };
142
167
  }
143
- const typedWidgetConfig = body.config;
168
+ const parsedWidgetConfig = StoredWidgetConfigSchema.safeParse(normalizeDashboardWidgetConfig(body.config));
169
+ if (!parsedWidgetConfig.success) {
170
+ response.setStatus(422);
171
+ return {
172
+ error: 'Invalid widget config',
173
+ validationErrors: formatWidgetConfigValidationErrors(parsedWidgetConfig.error),
174
+ };
175
+ }
176
+ const typedWidgetConfig = parsedWidgetConfig.data;
144
177
  const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
145
178
  if (apiValidationErrors.length) {
146
179
  response.setStatus(422);
147
180
  return {
148
181
  error: 'Invalid widget config',
149
- validationErrors: apiValidationErrors,
182
+ validationErrors: formatWidgetConfigApiValidationErrors(apiValidationErrors),
150
183
  };
151
184
  }
152
185
  return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { widgets: config.widgets.map((item) => item.id === widgetId
@@ -156,8 +189,8 @@ export function registerWidgetEndpoints(server, ctx) {
156
189
  server.endpoint({
157
190
  method: 'POST',
158
191
  path: '/dashboard/get_dashboard_widget_data',
159
- description: 'Loads query result data for one dashboard widget by dashboard slug and widget id.',
160
- request_schema: WidgetIdRequestSchema,
192
+ description: 'Loads widget data for one dashboard widget by dashboard slug and widget id.',
193
+ request_schema: WidgetDataRequestSchema,
161
194
  response_schema: DashboardWidgetDataResponseSchema,
162
195
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
163
196
  const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
@@ -175,7 +208,9 @@ export function registerWidgetEndpoints(server, ctx) {
175
208
  }
176
209
  return {
177
210
  widget,
178
- data: yield ctx.getWidgetData(widget),
211
+ data: yield ctx.getWidgetData(widget, {
212
+ pagination: body === null || body === void 0 ? void 0 : body.pagination,
213
+ }),
179
214
  };
180
215
  }),
181
216
  });