@adminforth/dashboard 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +116 -39
  2. package/custom/api/dashboardApi.ts +4 -0
  3. package/custom/composables/useElementSize.ts +17 -2
  4. package/custom/model/dashboard.types.ts +337 -236
  5. package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  6. package/custom/widgets/chart/ChartWidget.vue +38 -53
  7. package/custom/widgets/chart/bar/BarChart.vue +20 -12
  8. package/custom/widgets/chart/chart.types.ts +17 -66
  9. package/custom/widgets/chart/chart.utils.ts +11 -0
  10. package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  11. package/custom/widgets/chart/line/LineChart.vue +23 -15
  12. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  13. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  14. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  15. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  16. package/custom/widgets/table/TableWidget.vue +8 -3
  17. package/dist/custom/api/dashboardApi.d.ts +1 -0
  18. package/dist/custom/api/dashboardApi.js +5 -0
  19. package/dist/custom/api/dashboardApi.ts +4 -0
  20. package/dist/custom/composables/useElementSize.js +14 -2
  21. package/dist/custom/composables/useElementSize.ts +17 -2
  22. package/dist/custom/model/dashboard.types.d.ts +181 -61
  23. package/dist/custom/model/dashboard.types.js +82 -93
  24. package/dist/custom/model/dashboard.types.ts +337 -236
  25. package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
  26. package/dist/custom/queries/useWidgetData.d.ts +848 -62
  27. package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
  28. package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
  29. package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
  30. package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
  31. package/dist/custom/widgets/chart/chart.types.js +2 -25
  32. package/dist/custom/widgets/chart/chart.types.ts +17 -66
  33. package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
  34. package/dist/custom/widgets/chart/chart.utils.js +7 -0
  35. package/dist/custom/widgets/chart/chart.utils.ts +11 -0
  36. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
  37. package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
  38. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
  39. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
  40. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  41. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
  42. package/dist/custom/widgets/table/TableWidget.vue +8 -3
  43. package/dist/endpoint/dashboard.d.ts +7 -2
  44. package/dist/endpoint/dashboard.js +45 -1
  45. package/dist/endpoint/widgets.d.ts +2 -1
  46. package/dist/endpoint/widgets.js +6 -2
  47. package/dist/schema/api.d.ts +2773 -736
  48. package/dist/schema/api.js +5 -0
  49. package/dist/schema/widget.d.ts +1648 -476
  50. package/dist/schema/widget.js +208 -139
  51. package/dist/services/widgetConfigValidator.js +16 -40
  52. package/dist/services/widgetDataService.d.ts +2 -1
  53. package/dist/services/widgetDataService.js +389 -82
  54. package/endpoint/dashboard.ts +77 -4
  55. package/endpoint/widgets.ts +11 -4
  56. package/package.json +1 -1
  57. package/schema/api.ts +6 -0
  58. package/schema/widget.ts +225 -139
  59. package/services/widgetConfigValidator.ts +29 -53
  60. package/services/widgetDataService.ts +522 -100
@@ -85,7 +85,14 @@
85
85
  <script setup lang="ts">
86
86
  import { computed } from 'vue'
87
87
  import { useElementSize } from '../../../composables/useElementSize.js'
88
- import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
88
+ import {
89
+ CHART_COLORS,
90
+ formatChartAxisLabel,
91
+ formatChartLabel,
92
+ formatChartValue,
93
+ getChartYAxisWidth,
94
+ toFiniteNumber,
95
+ } from '../chart.utils.js'
89
96
 
90
97
  const props = withDefaults(defineProps<{
91
98
  rows: Record<string, unknown>[]
@@ -100,12 +107,6 @@ const props = withDefaults(defineProps<{
100
107
 
101
108
  const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
102
109
 
103
- const padding = {
104
- top: 12,
105
- right: 6,
106
- bottom: 24,
107
- left: 38,
108
- }
109
110
  const chartWidth = computed(() => Math.max(rootWidth.value, 1))
110
111
  const chartHeight = computed(() => {
111
112
  if (rootHeight.value > 0) {
@@ -118,14 +119,21 @@ const chartHeight = computed(() => {
118
119
  const chartColor = computed(() => props.color || CHART_COLORS[0])
119
120
  const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.yField])))
120
121
  const maxValue = computed(() => Math.max(...values.value, 1))
121
- const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
122
- const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
122
+ const yTickValues = computed(() => [maxValue.value, maxValue.value * 0.5, 0])
123
+ const padding = computed(() => ({
124
+ top: 12,
125
+ right: 6,
126
+ bottom: 24,
127
+ left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
128
+ }))
129
+ const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
130
+ const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
123
131
 
124
132
  const points = computed(() => {
125
133
  if (props.rows.length === 1) {
126
134
  return [{
127
- x: padding.left + innerWidth.value / 2,
128
- y: padding.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
135
+ x: padding.value.left + innerWidth.value / 2,
136
+ y: padding.value.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
129
137
  label: formatChartLabel(props.rows[0][props.xField]),
130
138
  axisLabel: formatChartAxisLabel(props.rows[0][props.xField]),
131
139
  value: values.value[0],
@@ -133,8 +141,8 @@ const points = computed(() => {
133
141
  }
134
142
 
135
143
  return props.rows.map((row, index) => ({
136
- x: padding.left + (index / (props.rows.length - 1)) * innerWidth.value,
137
- y: padding.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
144
+ x: padding.value.left + (index / (props.rows.length - 1)) * innerWidth.value,
145
+ y: padding.value.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
138
146
  label: formatChartLabel(row[props.xField]),
139
147
  axisLabel: formatChartAxisLabel(row[props.xField]),
140
148
  value: values.value[index],
@@ -152,12 +160,12 @@ const fillPath = computed(() => {
152
160
 
153
161
  const first = points.value[0]
154
162
  const last = points.value[points.value.length - 1]
155
- return `${linePath.value} L ${last.x} ${padding.top + innerHeight.value} L ${first.x} ${padding.top + innerHeight.value} Z`
163
+ return `${linePath.value} L ${last.x} ${padding.value.top + innerHeight.value} L ${first.x} ${padding.value.top + innerHeight.value} Z`
156
164
  })
157
165
 
158
166
  const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
159
167
  value: maxValue.value * (1 - ratio),
160
- y: padding.top + innerHeight.value * ratio,
168
+ y: padding.value.top + innerHeight.value * ratio,
161
169
  })))
162
170
 
163
171
  const xLabels = computed(() => {
@@ -1,13 +1,20 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { useElementSize } from '../../../composables/useElementSize.js'
4
- import type { ChartWidgetSeriesConfig } from '../chart.types.js'
5
- import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
4
+ import {
5
+ CHART_COLORS,
6
+ formatChartAxisLabel,
7
+ formatChartLabel,
8
+ formatChartValue,
9
+ getChartYAxisWidth,
10
+ toFiniteNumber,
11
+ } from '../chart.utils.js'
6
12
 
7
13
  const props = withDefaults(defineProps<{
8
14
  rows: Record<string, unknown>[]
9
15
  xField: string
10
- series: ChartWidgetSeriesConfig[]
16
+ yField: string
17
+ seriesField: string
11
18
  colors?: string[]
12
19
  height?: number
13
20
  }>(), {
@@ -17,16 +24,11 @@ const props = withDefaults(defineProps<{
17
24
  const { el: rootEl, width: rootWidth } = useElementSize<HTMLDivElement>()
18
25
  const { el: svgEl, width: svgWidth, height: svgHeight } = useElementSize<HTMLDivElement>()
19
26
 
20
- const padding = {
21
- top: 24,
22
- right: 6,
23
- bottom: 34,
24
- left: 38,
25
- }
26
27
  const barGap = 10
27
- const normalizedSeries = computed(() => props.series.map((series, index) => ({
28
- ...series,
29
- color: series.color || props.colors?.[index] || CHART_COLORS[index % CHART_COLORS.length],
28
+ const seriesNames = computed(() => Array.from(new Set(props.rows.map((row) => formatChartLabel(row[props.seriesField])))))
29
+ const normalizedSeries = computed(() => seriesNames.value.map((name, index) => ({
30
+ name,
31
+ color: props.colors?.[index] || CHART_COLORS[index % CHART_COLORS.length],
30
32
  })))
31
33
  const showLegend = computed(() => normalizedSeries.value.length > 0)
32
34
  const isCompact = computed(() => rootWidth.value > 0 && rootWidth.value < 420)
@@ -38,19 +40,15 @@ const chartHeight = computed(() => {
38
40
 
39
41
  return Math.max(props.height - (showLegend.value ? 28 : 0), 96)
40
42
  })
41
- const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
42
- const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
43
43
  const groupedRows = computed(() => {
44
44
  const grouped = new Map<string, Record<string, unknown>>()
45
45
 
46
46
  for (const row of props.rows) {
47
47
  const label = formatChartLabel(row[props.xField])
48
48
  const item = grouped.get(label) ?? { [props.xField]: label }
49
+ const seriesName = formatChartLabel(row[props.seriesField])
49
50
 
50
- for (const series of normalizedSeries.value) {
51
- item[series.name] = toFiniteNumber(item[series.name])
52
- + getSeriesContribution(row[series.field], series.name)
53
- }
51
+ item[seriesName] = toFiniteNumber(item[seriesName]) + toFiniteNumber(row[props.yField])
54
52
 
55
53
  grouped.set(label, item)
56
54
  }
@@ -65,15 +63,24 @@ const totalChartWidth = computed(() => {
65
63
  const count = Math.max(groupedRows.value.length, 1)
66
64
  return count * barWidth.value + (count - 1) * barGap
67
65
  })
68
- const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
66
+ const chartStartX = computed(() => padding.value.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
69
67
  const totals = computed(() => groupedRows.value.map((row) => normalizedSeries.value.reduce(
70
68
  (sum, series) => sum + toFiniteNumber(row[series.name]),
71
69
  0,
72
70
  )))
73
71
  const maxTotal = computed(() => Math.max(...totals.value, 1))
72
+ const yTickValues = computed(() => [maxTotal.value, maxTotal.value * 0.5, 0])
73
+ const padding = computed(() => ({
74
+ top: 24,
75
+ right: 6,
76
+ bottom: 34,
77
+ left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
78
+ }))
79
+ const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
80
+ const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
74
81
 
75
82
  const bars = computed(() => groupedRows.value.map((row, rowIndex) => {
76
- let y = padding.top + innerHeight.value
83
+ let y = padding.value.top + innerHeight.value
77
84
 
78
85
  return {
79
86
  label: String(row[props.xField]),
@@ -118,31 +125,9 @@ const visibleLabelIndexes = computed(() => {
118
125
 
119
126
  const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
120
127
  value: maxTotal.value * (1 - ratio),
121
- y: padding.top + innerHeight.value * ratio,
128
+ y: padding.value.top + innerHeight.value * ratio,
122
129
  })))
123
130
 
124
- function getSeriesContribution(value: unknown, seriesName: string) {
125
- if (typeof value === 'boolean') {
126
- return value === getBooleanSeriesValue(seriesName) ? 1 : 0
127
- }
128
-
129
- if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
130
- return (value.toLowerCase() === 'true') === getBooleanSeriesValue(seriesName) ? 1 : 0
131
- }
132
-
133
- return toFiniteNumber(value)
134
- }
135
-
136
- function getBooleanSeriesValue(seriesName: string) {
137
- const normalizedName = seriesName.toLowerCase()
138
-
139
- if (normalizedName.includes('unlisted') || normalizedName.includes('false')) {
140
- return false
141
- }
142
-
143
- return true
144
- }
145
-
146
131
  function getBarTooltip(bar: { label: string, total: number, segments: Array<{ name: string, value: number }> }) {
147
132
  const percentFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 })
148
133
 
@@ -1,9 +1,6 @@
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'
7
4
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
8
5
  import { CHART_COLORS, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
9
6
 
@@ -55,20 +52,18 @@ watch(
55
52
  { deep: true },
56
53
  )
57
54
 
58
- const gaugeConfig = computed(() => normalizeGaugeCardWidgetConfig(props.widget.gauge_card))
55
+ const gaugeConfig = computed(() => props.widget.target === 'gauge_card' ? props.widget.card : undefined)
59
56
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
60
57
  const columns = computed(() => widgetData.value?.columns ?? [])
61
58
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
62
- const valueField = computed(() => gaugeConfig.value?.valueField || columns.value[0])
63
- const minField = computed(() => gaugeConfig.value?.minField)
64
- const maxField = computed(() => gaugeConfig.value?.maxField)
59
+ const valueField = computed(() => gaugeConfig.value?.value.field || columns.value[0])
60
+ const targetField = computed(() => gaugeConfig.value?.target?.field ?? gaugeConfig.value?.progress?.targetField)
65
61
  const minValue = computed(() => {
66
- const dynamicMin = minField.value ? parseOptionalNumber(firstRow.value[minField.value]) : undefined
67
- return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
62
+ return 0
68
63
  })
69
64
  const maxValue = computed(() => {
70
- const dynamicMax = maxField.value ? parseOptionalNumber(firstRow.value[maxField.value]) : undefined
71
- return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.max) ?? 100
65
+ const dynamicMax = targetField.value ? parseOptionalNumber(firstRow.value[targetField.value]) : undefined
66
+ return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.target?.value ?? gaugeConfig.value?.progress?.targetValue) ?? 100
72
67
  })
73
68
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
74
69
  const fractionDigits = computed(() => Math.min([
@@ -148,7 +143,7 @@ const gaugeColor = computed(() => gaugeConfig.value?.color || CHART_COLORS[0])
148
143
  </svg>
149
144
 
150
145
  <div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
151
- {{ formattedValue }}{{ gaugeConfig?.suffix ?? '' }}
146
+ {{ gaugeConfig?.value.prefix ?? '' }}{{ formattedValue }}{{ gaugeConfig?.value.suffix ?? '' }}
152
147
  </div>
153
148
  <div class="text-sm text-lightListTableText dark:text-darkListTableText">
154
149
  {{ formattedMinValue }} - {{ formattedMaxValue }}
@@ -1,9 +1,6 @@
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'
7
4
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
8
5
  import { formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
9
6
 
@@ -29,15 +26,16 @@ watch(
29
26
  { deep: true },
30
27
  )
31
28
 
32
- const kpiConfig = computed(() => normalizeKpiCardWidgetConfig(props.widget.kpi_card))
29
+ const kpiConfig = computed(() => props.widget.target === 'kpi_card' ? props.widget.card : undefined)
33
30
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
34
31
  const columns = computed(() => widgetData.value?.columns ?? [])
35
32
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
36
- const valueField = computed(() => kpiConfig.value?.valueField || columns.value[0])
37
- const labelField = computed(() => kpiConfig.value?.labelField)
33
+ const valueField = computed(() => kpiConfig.value?.value.field || columns.value[0])
38
34
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
39
- const label = computed(() => labelField.value ? String(firstRow.value[labelField.value]) : props.widget.label)
40
- const formattedValue = computed(() => `${kpiConfig.value?.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.suffix ?? ''}`)
35
+ const label = computed(() => kpiConfig.value?.subtitle?.field
36
+ ? String(firstRow.value[kpiConfig.value.subtitle.field])
37
+ : kpiConfig.value?.subtitle?.text ?? kpiConfig.value?.title ?? props.widget.label)
38
+ const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.value.suffix ?? ''}`)
41
39
  </script>
42
40
 
43
41
  <template>
@@ -2,7 +2,7 @@
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
4
  import {
5
- normalizePivotTableWidgetConfig,
5
+ getFieldRefField,
6
6
  } from '../../model/dashboard.types.js'
7
7
  import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
8
8
  import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
@@ -29,16 +29,17 @@ watch(
29
29
  { deep: true },
30
30
  )
31
31
 
32
- const pivotConfig = computed(() => normalizePivotTableWidgetConfig(props.widget.pivot_table))
32
+ const pivotConfig = computed(() => props.widget.target === 'pivot_table' ? props.widget.pivot : undefined)
33
33
  const widgetData = computed(() => data.value?.data as DashboardWidgetData | null)
34
34
  const rows = computed(() => widgetData.value?.rows ?? [])
35
35
  const columns = computed(() => widgetData.value?.columns ?? [])
36
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
- const aggregation = computed(() => pivotConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
37
+ const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.columns?.length)
38
+ const rowField = computed(() => getFieldRefField(pivotConfig.value?.rows[0]) || columns.value[0])
39
+ const columnField = computed(() => getFieldRefField(pivotConfig.value?.columns?.[0]) || columns.value[1])
40
+ const valueConfig = computed(() => pivotConfig.value?.values[0])
41
+ const valueField = computed(() => valueConfig.value?.field || columns.value[2] || columns.value[1])
42
+ const aggregation = computed(() => valueConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
42
43
  const pivotColumnLabels = computed(() => {
43
44
  if (shouldRenderAggregateMatrix.value) {
44
45
  return columns.value.filter((column) => column !== rowField.value)
@@ -113,10 +113,11 @@
113
113
  <script setup lang="ts">
114
114
  import { computed, ref, watch } from 'vue'
115
115
  import { useWidgetData } from '../../queries/useWidgetData.js'
116
- import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
116
+ import { getFieldRefField } from '../../model/dashboard.types.js'
117
+ import type { DashboardWidgetConfig, DashboardWidgetTableData, FieldRef } from '../../model/dashboard.types.js'
117
118
 
118
119
  type TableWidgetConfig = {
119
- columns?: string[]
120
+ columns?: FieldRef[]
120
121
  pagination?: boolean
121
122
  pageSize?: number
122
123
  }
@@ -168,7 +169,11 @@ const tableData = computed(() => {
168
169
 
169
170
  const columns = computed(() => {
170
171
  const configuredColumns = tableConfig.value?.columns
171
- return configuredColumns ?? tableData.value?.columns ?? []
172
+ if (configuredColumns) {
173
+ return configuredColumns.map((column) => getFieldRefField(column)).filter(Boolean) as string[]
174
+ }
175
+
176
+ return tableData.value?.columns ?? []
172
177
  })
173
178
 
174
179
  const pagination = computed(() => tableData.value?.pagination)
@@ -1,7 +1,12 @@
1
- import type { IHttpServer } from 'adminforth';
2
- import type { DashboardRecord } from '../services/dashboardConfigService.js';
1
+ import type { AdminUser, IHttpServer } from 'adminforth';
2
+ import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
+ import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
+ import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
3
5
  type DashboardEndpointsContext = {
6
+ canEditDashboard: (adminUser: AdminUser) => boolean;
4
7
  getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
8
+ persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
9
+ validateDashboardWidgetApiConfig: (widget: DashboardWidgetConfig) => DashboardWidgetConfigValidationError[];
5
10
  };
6
11
  export declare function registerDashboardEndpoints(server: IHttpServer, ctx: DashboardEndpointsContext): void;
7
12
  export {};
@@ -7,8 +7,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { DashboardApiResponseSchema, SlugRequestSchema } from '../schema/api.js';
10
+ import { normalizeDashboardConfig } from '../custom/model/dashboard.types.js';
11
+ import { DashboardApiResponseSchema, DashboardConfigZodSchema, SetDashboardConfigRequestSchema, SlugRequestSchema, } from '../schema/api.js';
11
12
  import { buildDashboardResponse } from '../services/dashboardConfigService.js';
13
+ function formatDashboardConfigValidationErrors(error) {
14
+ return error.issues.map((issue) => ({
15
+ field: issue.path.length ? issue.path.map(String).join('.') : 'config',
16
+ message: issue.message,
17
+ }));
18
+ }
12
19
  export function registerDashboardEndpoints(server, ctx) {
13
20
  server.endpoint({
14
21
  method: 'POST',
@@ -26,4 +33,41 @@ export function registerDashboardEndpoints(server, ctx) {
26
33
  return buildDashboardResponse(dashboard);
27
34
  }),
28
35
  });
36
+ server.endpoint({
37
+ method: 'POST',
38
+ path: '/dashboard/set_dashboard_config',
39
+ description: 'Replaces one dashboard configuration, including groups and widgets. Superadmin only.',
40
+ request_schema: SetDashboardConfigRequestSchema,
41
+ response_schema: DashboardApiResponseSchema,
42
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
43
+ if (!ctx.canEditDashboard(adminUser)) {
44
+ response.setStatus(403);
45
+ return { error: 'Dashboard edit is not allowed' };
46
+ }
47
+ const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
48
+ const dashboard = yield ctx.getDashboardRecord(slug);
49
+ if (!dashboard) {
50
+ response.setStatus(404);
51
+ return { error: 'Dashboard not found' };
52
+ }
53
+ const normalizedConfig = normalizeDashboardConfig(body === null || body === void 0 ? void 0 : body.config);
54
+ const parsedConfig = DashboardConfigZodSchema.safeParse(normalizedConfig);
55
+ if (!parsedConfig.success) {
56
+ response.setStatus(422);
57
+ return {
58
+ error: 'Invalid dashboard config',
59
+ validationErrors: formatDashboardConfigValidationErrors(parsedConfig.error),
60
+ };
61
+ }
62
+ const widgetValidationErrors = parsedConfig.data.widgets.flatMap((widget, index) => (ctx.validateDashboardWidgetApiConfig(widget).map((error) => (Object.assign(Object.assign({}, error), { field: `widgets.${index}.${error.field}` })))));
63
+ if (widgetValidationErrors.length) {
64
+ response.setStatus(422);
65
+ return {
66
+ error: 'Invalid dashboard config',
67
+ validationErrors: widgetValidationErrors,
68
+ };
69
+ }
70
+ return ctx.persistDashboardConfig(dashboard, parsedConfig.data);
71
+ }),
72
+ });
29
73
  }
@@ -1,5 +1,5 @@
1
1
  import type { AdminUser, IHttpServer } from 'adminforth';
2
- import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
2
+ import type { DashboardConfig, DashboardVariables, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
3
3
  import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
4
4
  import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
5
5
  type WidgetEndpointsContext = {
@@ -14,6 +14,7 @@ type WidgetEndpointsContext = {
14
14
  page: number;
15
15
  pageSize: number;
16
16
  };
17
+ variables?: DashboardVariables;
17
18
  }) => Promise<unknown>;
18
19
  };
19
20
  export declare function registerWidgetEndpoints(server: IHttpServer, ctx: WidgetEndpointsContext): void;
@@ -24,10 +24,13 @@ function formatWidgetConfigFieldPath(field) {
24
24
  const fieldAliases = new Map([
25
25
  ['minWidth', 'min_width'],
26
26
  ['maxWidth', 'max_width'],
27
- ['dataSource', 'data_source'],
28
- ['resourceId', 'resource_id'],
29
27
  ['groupBy', 'group_by'],
28
+ ['orderBy', 'order_by'],
30
29
  ['pageSize', 'page_size'],
30
+ ['timeSeries', 'time_series'],
31
+ ['valueField', 'value_field'],
32
+ ['targetValue', 'target_value'],
33
+ ['targetField', 'target_field'],
31
34
  ]);
32
35
  return field
33
36
  .split('.')
@@ -210,6 +213,7 @@ export function registerWidgetEndpoints(server, ctx) {
210
213
  widget,
211
214
  data: yield ctx.getWidgetData(widget, {
212
215
  pagination: body === null || body === void 0 ? void 0 : body.pagination,
216
+ variables: widget.variables,
213
217
  }),
214
218
  };
215
219
  }),