@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.
- package/README.md +99 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +353 -2
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/custom/widgets/chart/ChartWidget.vue +45 -12
- package/custom/widgets/chart/chart.types.ts +83 -0
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/custom/api/dashboardApi.d.ts +7 -1
- package/dist/custom/api/dashboardApi.js +4 -6
- package/dist/custom/api/dashboardApi.ts +9 -0
- package/dist/custom/model/dashboard.types.d.ts +70 -1
- package/dist/custom/model/dashboard.types.js +173 -1
- package/dist/custom/model/dashboard.types.ts +353 -2
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -2
- package/dist/custom/queries/useWidgetData.d.ts +44 -3
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
- package/dist/custom/widgets/chart/chart.types.js +46 -0
- package/dist/custom/widgets/chart/chart.types.ts +83 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -1
- package/dist/custom/widgets/chart/chart.utils.js +2 -2
- package/dist/custom/widgets/chart/chart.utils.ts +2 -2
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +63 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +32 -12
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +41 -6
- package/dist/schema/api.d.ts +874 -444
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +538 -132
- package/dist/schema/widget.js +138 -14
- package/dist/services/widgetConfigValidator.js +26 -40
- package/dist/services/widgetDataService.d.ts +7 -14
- package/dist/services/widgetDataService.js +115 -11
- package/endpoint/widgets.ts +56 -6
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +145 -15
- package/services/widgetConfigValidator.ts +36 -44
- 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
|
|
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?.
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
-
{{
|
|
151
|
+
{{ formattedValue }}{{ gaugeConfig?.suffix ?? '' }}
|
|
101
152
|
</div>
|
|
102
153
|
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
103
|
-
{{
|
|
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
|
|
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?.
|
|
39
|
-
const labelField = computed(() => kpiConfig.value?.
|
|
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
|
|
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
|
|
30
|
-
|
|
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
|
|
39
|
-
const
|
|
40
|
-
const
|
|
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(() =>
|
|
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
|
|
26
|
+
class="flex min-h-0 flex-1 flex-col"
|
|
27
27
|
>
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
<
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
>
|
|
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 =
|
|
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
|
|
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 {};
|
package/dist/endpoint/widgets.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
160
|
-
request_schema:
|
|
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
|
});
|