@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.
- package/README.md +116 -39
- package/custom/api/dashboardApi.ts +4 -0
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +337 -236
- package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/custom/widgets/chart/ChartWidget.vue +38 -53
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +17 -66
- package/custom/widgets/chart/chart.utils.ts +11 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/custom/widgets/chart/line/LineChart.vue +23 -15
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/custom/api/dashboardApi.d.ts +1 -0
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +4 -0
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +181 -61
- package/dist/custom/model/dashboard.types.js +82 -93
- package/dist/custom/model/dashboard.types.ts +337 -236
- package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
- package/dist/custom/queries/useWidgetData.d.ts +848 -62
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
- package/dist/custom/widgets/chart/chart.types.js +2 -25
- package/dist/custom/widgets/chart/chart.types.ts +17 -66
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
- package/dist/custom/widgets/chart/chart.utils.js +7 -0
- package/dist/custom/widgets/chart/chart.utils.ts +11 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/dist/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/endpoint/dashboard.d.ts +7 -2
- package/dist/endpoint/dashboard.js +45 -1
- package/dist/endpoint/widgets.d.ts +2 -1
- package/dist/endpoint/widgets.js +6 -2
- package/dist/schema/api.d.ts +2773 -736
- package/dist/schema/api.js +5 -0
- package/dist/schema/widget.d.ts +1648 -476
- package/dist/schema/widget.js +208 -139
- package/dist/services/widgetConfigValidator.js +16 -40
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +389 -82
- package/endpoint/dashboard.ts +77 -4
- package/endpoint/widgets.ts +11 -4
- package/package.json +1 -1
- package/schema/api.ts +6 -0
- package/schema/widget.ts +225 -139
- package/services/widgetConfigValidator.ts +29 -53
- 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 {
|
|
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
|
|
122
|
-
const
|
|
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
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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?.
|
|
63
|
-
const
|
|
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
|
-
|
|
67
|
-
return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
|
|
62
|
+
return 0
|
|
68
63
|
})
|
|
69
64
|
const maxValue = computed(() => {
|
|
70
|
-
const dynamicMax =
|
|
71
|
-
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.
|
|
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(() =>
|
|
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?.
|
|
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(() =>
|
|
40
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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?.
|
|
38
|
-
const rowField = computed(() => pivotConfig.value?.
|
|
39
|
-
const columnField = computed(() => pivotConfig.value?.
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
|
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?:
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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;
|
package/dist/endpoint/widgets.js
CHANGED
|
@@ -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
|
}),
|