@adminforth/dashboard 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -54
- package/custom/api/dashboardApi.ts +9 -0
- package/custom/model/dashboard.types.ts +158 -1
- package/custom/queries/useWidgetData.ts +8 -4
- package/custom/runtime/WidgetShell.vue +8 -4
- package/custom/widgets/chart/chart.utils.ts +2 -2
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +94 -12
- package/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- 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 +45 -0
- package/dist/custom/model/dashboard.types.js +82 -1
- package/dist/custom/model/dashboard.types.ts +158 -1
- package/dist/custom/queries/useDashboardConfig.d.ts +42 -0
- package/dist/custom/queries/useWidgetData.d.ts +44 -1
- package/dist/custom/queries/useWidgetData.js +3 -3
- package/dist/custom/queries/useWidgetData.ts +8 -4
- package/dist/custom/runtime/WidgetShell.vue +8 -4
- 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 +94 -12
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +27 -5
- package/dist/custom/widgets/table/TableWidget.vue +155 -30
- package/dist/endpoint/widgets.d.ts +6 -1
- package/dist/endpoint/widgets.js +22 -4
- package/dist/schema/api.d.ts +882 -212
- package/dist/schema/api.js +11 -2
- package/dist/schema/widget.d.ts +542 -4
- package/dist/schema/widget.js +111 -1
- package/dist/services/widgetConfigValidator.js +32 -6
- package/dist/services/widgetDataService.d.ts +8 -6
- package/dist/services/widgetDataService.js +133 -11
- package/endpoint/widgets.ts +31 -4
- package/package.json +1 -1
- package/schema/api.ts +11 -1
- package/schema/widget.ts +114 -1
- package/services/widgetConfigValidator.ts +45 -6
- package/services/widgetDataService.ts +201 -19
|
@@ -23,6 +23,27 @@ export declare function useDashboardConfig(slug: Ref<string>): {
|
|
|
23
23
|
maxWidth?: number | null | undefined;
|
|
24
24
|
order: number;
|
|
25
25
|
target: import("../model/dashboard.types.js").DashboardWidgetTarget;
|
|
26
|
+
dataSource?: {
|
|
27
|
+
type: "resource";
|
|
28
|
+
resourceId: string;
|
|
29
|
+
columns?: string[] | undefined;
|
|
30
|
+
sort?: unknown;
|
|
31
|
+
filters?: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
type: "aggregate";
|
|
34
|
+
resourceId: string;
|
|
35
|
+
aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
|
|
36
|
+
groupBy?: {
|
|
37
|
+
type: "field";
|
|
38
|
+
field: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: "date_trunc";
|
|
41
|
+
field: string;
|
|
42
|
+
truncation: "day" | "week" | "month" | "year";
|
|
43
|
+
timezone?: string | undefined;
|
|
44
|
+
} | undefined;
|
|
45
|
+
filters?: unknown;
|
|
46
|
+
} | undefined;
|
|
26
47
|
chart?: {
|
|
27
48
|
type: import("../widgets/chart/chart.types.js").ChartWidgetType;
|
|
28
49
|
title?: string | undefined;
|
|
@@ -75,6 +96,27 @@ export declare function useDashboardConfig(slug: Ref<string>): {
|
|
|
75
96
|
maxWidth?: number | null | undefined;
|
|
76
97
|
order: number;
|
|
77
98
|
target: import("../model/dashboard.types.js").DashboardWidgetTarget;
|
|
99
|
+
dataSource?: {
|
|
100
|
+
type: "resource";
|
|
101
|
+
resourceId: string;
|
|
102
|
+
columns?: string[] | undefined;
|
|
103
|
+
sort?: unknown;
|
|
104
|
+
filters?: unknown;
|
|
105
|
+
} | {
|
|
106
|
+
type: "aggregate";
|
|
107
|
+
resourceId: string;
|
|
108
|
+
aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
|
|
109
|
+
groupBy?: {
|
|
110
|
+
type: "field";
|
|
111
|
+
field: string;
|
|
112
|
+
} | {
|
|
113
|
+
type: "date_trunc";
|
|
114
|
+
field: string;
|
|
115
|
+
truncation: "day" | "week" | "month" | "year";
|
|
116
|
+
timezone?: string | undefined;
|
|
117
|
+
} | undefined;
|
|
118
|
+
filters?: unknown;
|
|
119
|
+
} | undefined;
|
|
78
120
|
chart?: {
|
|
79
121
|
type: import("../widgets/chart/chart.types.js").ChartWidgetType;
|
|
80
122
|
title?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Ref } from 'vue';
|
|
2
|
-
|
|
2
|
+
import { type DashboardWidgetDataRequest } from '../api/dashboardApi.js';
|
|
3
|
+
export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>, request?: Ref<DashboardWidgetDataRequest>): {
|
|
3
4
|
data: Ref<{
|
|
4
5
|
widget: {
|
|
5
6
|
id: string;
|
|
@@ -12,6 +13,27 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
|
|
|
12
13
|
maxWidth?: number | null | undefined;
|
|
13
14
|
order: number;
|
|
14
15
|
target: import("../model/dashboard.types.js").DashboardWidgetTarget;
|
|
16
|
+
dataSource?: {
|
|
17
|
+
type: "resource";
|
|
18
|
+
resourceId: string;
|
|
19
|
+
columns?: string[] | undefined;
|
|
20
|
+
sort?: unknown;
|
|
21
|
+
filters?: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
type: "aggregate";
|
|
24
|
+
resourceId: string;
|
|
25
|
+
aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
|
|
26
|
+
groupBy?: {
|
|
27
|
+
type: "field";
|
|
28
|
+
field: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "date_trunc";
|
|
31
|
+
field: string;
|
|
32
|
+
truncation: "day" | "week" | "month" | "year";
|
|
33
|
+
timezone?: string | undefined;
|
|
34
|
+
} | undefined;
|
|
35
|
+
filters?: unknown;
|
|
36
|
+
} | undefined;
|
|
15
37
|
chart?: {
|
|
16
38
|
type: import("../widgets/chart/chart.types.js").ChartWidgetType;
|
|
17
39
|
title?: string | undefined;
|
|
@@ -53,6 +75,27 @@ export declare function useWidgetData(slug: Ref<string>, widgetId: Ref<string>):
|
|
|
53
75
|
maxWidth?: number | null | undefined;
|
|
54
76
|
order: number;
|
|
55
77
|
target: import("../model/dashboard.types.js").DashboardWidgetTarget;
|
|
78
|
+
dataSource?: {
|
|
79
|
+
type: "resource";
|
|
80
|
+
resourceId: string;
|
|
81
|
+
columns?: string[] | undefined;
|
|
82
|
+
sort?: unknown;
|
|
83
|
+
filters?: unknown;
|
|
84
|
+
} | {
|
|
85
|
+
type: "aggregate";
|
|
86
|
+
resourceId: string;
|
|
87
|
+
aggregations: Record<string, import("../model/dashboard.types.js").AggregationRule>;
|
|
88
|
+
groupBy?: {
|
|
89
|
+
type: "field";
|
|
90
|
+
field: string;
|
|
91
|
+
} | {
|
|
92
|
+
type: "date_trunc";
|
|
93
|
+
field: string;
|
|
94
|
+
truncation: "day" | "week" | "month" | "year";
|
|
95
|
+
timezone?: string | undefined;
|
|
96
|
+
} | undefined;
|
|
97
|
+
filters?: unknown;
|
|
98
|
+
} | undefined;
|
|
56
99
|
chart?: {
|
|
57
100
|
type: import("../widgets/chart/chart.types.js").ChartWidgetType;
|
|
58
101
|
title?: string | undefined;
|
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.useWidgetData = useWidgetData;
|
|
13
13
|
const vue_1 = require("vue");
|
|
14
14
|
const dashboardApi_js_1 = require("../api/dashboardApi.js");
|
|
15
|
-
function useWidgetData(slug, widgetId) {
|
|
15
|
+
function useWidgetData(slug, widgetId, request) {
|
|
16
16
|
const data = (0, vue_1.ref)(null);
|
|
17
17
|
const isLoading = (0, vue_1.ref)(false);
|
|
18
18
|
const isFetching = (0, vue_1.ref)(false);
|
|
@@ -29,7 +29,7 @@ function useWidgetData(slug, widgetId) {
|
|
|
29
29
|
isLoading.value = true;
|
|
30
30
|
}
|
|
31
31
|
try {
|
|
32
|
-
const response = yield dashboardApi_js_1.dashboardApi.getDashboardWidgetData(slug.value, widgetId.value);
|
|
32
|
+
const response = yield dashboardApi_js_1.dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request === null || request === void 0 ? void 0 : request.value);
|
|
33
33
|
data.value = response;
|
|
34
34
|
error.value = null;
|
|
35
35
|
return response;
|
|
@@ -44,7 +44,7 @@ function useWidgetData(slug, widgetId) {
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
-
(0, vue_1.watch)([slug, widgetId], () => {
|
|
47
|
+
(0, vue_1.watch)(request ? [slug, widgetId, request] : [slug, widgetId], () => {
|
|
48
48
|
void refetch();
|
|
49
49
|
}, { immediate: true });
|
|
50
50
|
return {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ref, watch, type Ref } from 'vue'
|
|
2
|
-
import { dashboardApi } from '../api/dashboardApi.js'
|
|
2
|
+
import { dashboardApi, type DashboardWidgetDataRequest } from '../api/dashboardApi.js'
|
|
3
3
|
|
|
4
|
-
export function useWidgetData(
|
|
4
|
+
export function useWidgetData(
|
|
5
|
+
slug: Ref<string>,
|
|
6
|
+
widgetId: Ref<string>,
|
|
7
|
+
request?: Ref<DashboardWidgetDataRequest>,
|
|
8
|
+
) {
|
|
5
9
|
const data = ref<Awaited<ReturnType<typeof dashboardApi.getDashboardWidgetData>> | null>(null)
|
|
6
10
|
const isLoading = ref(false)
|
|
7
11
|
const isFetching = ref(false)
|
|
@@ -20,7 +24,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
try {
|
|
23
|
-
const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value)
|
|
27
|
+
const response = await dashboardApi.getDashboardWidgetData(slug.value, widgetId.value, request?.value)
|
|
24
28
|
data.value = response
|
|
25
29
|
error.value = null
|
|
26
30
|
return response
|
|
@@ -34,7 +38,7 @@ export function useWidgetData(slug: Ref<string>, widgetId: Ref<string>) {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
watch(
|
|
37
|
-
[slug, widgetId],
|
|
41
|
+
request ? [slug, widgetId, request] : [slug, widgetId],
|
|
38
42
|
() => {
|
|
39
43
|
void refetch()
|
|
40
44
|
},
|
|
@@ -135,11 +135,11 @@ const widgetLayoutVars = computed<CSSProperties>(() => {
|
|
|
135
135
|
const fixedWidth = formatWidth(props.layout?.width)
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
|
-
'--widget-basis': fixedWidth ?? basis,
|
|
139
|
-
'--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
|
|
138
|
+
'--widget-basis': clampToContainerWidth(fixedWidth ?? basis),
|
|
139
|
+
'--widget-min-width': clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis),
|
|
140
140
|
'--widget-max-width': props.layout?.maxWidth === null
|
|
141
|
-
? '
|
|
142
|
-
: fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '
|
|
141
|
+
? '100%'
|
|
142
|
+
: clampToContainerWidth(fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? '100%'),
|
|
143
143
|
height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
|
|
144
144
|
}
|
|
145
145
|
})
|
|
@@ -149,4 +149,8 @@ function formatWidth(value: number | undefined) {
|
|
|
149
149
|
return `${value}px`
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
function clampToContainerWidth(value: string) {
|
|
154
|
+
return `min(${value}, 100%)`
|
|
155
|
+
}
|
|
152
156
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const CHART_COLORS: string[];
|
|
2
2
|
export declare function toFiniteNumber(value: unknown): number;
|
|
3
|
-
export declare function formatChartValue(value: number): string;
|
|
3
|
+
export declare function formatChartValue(value: number, options?: Intl.NumberFormatOptions): string;
|
|
4
4
|
export declare function formatChartLabel(value: unknown): string;
|
|
5
5
|
export declare function formatChartAxisLabel(value: unknown, maxLength?: number): string;
|
|
@@ -19,8 +19,8 @@ function toFiniteNumber(value) {
|
|
|
19
19
|
const numberValue = typeof value === 'number' ? value : Number(value);
|
|
20
20
|
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
21
21
|
}
|
|
22
|
-
function formatChartValue(value) {
|
|
23
|
-
return new Intl.NumberFormat().format(value);
|
|
22
|
+
function formatChartValue(value, options = {}) {
|
|
23
|
+
return new Intl.NumberFormat(undefined, options).format(value);
|
|
24
24
|
}
|
|
25
25
|
function formatChartLabel(value) {
|
|
26
26
|
if (typeof value !== 'string') {
|
|
@@ -14,8 +14,8 @@ export function toFiniteNumber(value: unknown) {
|
|
|
14
14
|
return Number.isFinite(numberValue) ? numberValue : 0
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function formatChartValue(value: number) {
|
|
18
|
-
return new Intl.NumberFormat().format(value)
|
|
17
|
+
export function formatChartValue(value: number, options: Intl.NumberFormatOptions = {}) {
|
|
18
|
+
return new Intl.NumberFormat(undefined, options).format(value)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function formatChartLabel(value: unknown) {
|
|
@@ -18,6 +18,66 @@ const {
|
|
|
18
18
|
refetch,
|
|
19
19
|
} = useWidgetData(dashboardSlugRef, widgetIdRef)
|
|
20
20
|
|
|
21
|
+
type GaugeCardConfig = {
|
|
22
|
+
value_field?: string
|
|
23
|
+
valueField?: string
|
|
24
|
+
min?: number | string
|
|
25
|
+
max?: number | string
|
|
26
|
+
min_field?: string
|
|
27
|
+
minField?: string
|
|
28
|
+
max_field?: string
|
|
29
|
+
maxField?: string
|
|
30
|
+
suffix?: string
|
|
31
|
+
color?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === 'object' && value !== null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseGaugeCardConfig(value: unknown): GaugeCardConfig | undefined {
|
|
39
|
+
if (isRecord(value)) {
|
|
40
|
+
return value as GaugeCardConfig
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(value) as unknown
|
|
49
|
+
return isRecord(parsed) ? parsed as GaugeCardConfig : undefined
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseOptionalNumber(value: unknown): number | undefined {
|
|
56
|
+
if (value === null || value === undefined || value === '') {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parsed = typeof value === 'number' ? value : Number(value)
|
|
61
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function countFractionDigits(value: number) {
|
|
65
|
+
if (!Number.isFinite(value)) {
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedValue = value.toString().toLowerCase()
|
|
70
|
+
const [coefficient, exponentValue] = normalizedValue.split('e')
|
|
71
|
+
const exponent = exponentValue ? Number(exponentValue) : 0
|
|
72
|
+
const decimalDigits = coefficient.split('.')[1]?.length ?? 0
|
|
73
|
+
|
|
74
|
+
return Math.max(decimalDigits - exponent, 0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeDisplayValue(value: number, useWholeNumbers: boolean) {
|
|
78
|
+
return useWholeNumbers ? Math.trunc(value) : value
|
|
79
|
+
}
|
|
80
|
+
|
|
21
81
|
watch(
|
|
22
82
|
() => props.widget,
|
|
23
83
|
() => {
|
|
@@ -26,20 +86,42 @@ watch(
|
|
|
26
86
|
{ deep: true },
|
|
27
87
|
)
|
|
28
88
|
|
|
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)
|
|
89
|
+
const gaugeConfig = computed(() => parseGaugeCardConfig(props.widget.gauge_card))
|
|
36
90
|
const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
37
91
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
38
92
|
const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
|
|
39
|
-
const valueField = computed(() => gaugeConfig.value?.value_field || columns.value[0])
|
|
40
|
-
const
|
|
41
|
-
const
|
|
93
|
+
const valueField = computed(() => gaugeConfig.value?.value_field || gaugeConfig.value?.valueField || columns.value[0])
|
|
94
|
+
const minField = computed(() => gaugeConfig.value?.min_field || gaugeConfig.value?.minField)
|
|
95
|
+
const maxField = computed(() => gaugeConfig.value?.max_field || gaugeConfig.value?.maxField)
|
|
96
|
+
const minValue = computed(() => {
|
|
97
|
+
const dynamicMin = minField.value ? parseOptionalNumber(firstRow.value[minField.value]) : undefined
|
|
98
|
+
return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
|
|
99
|
+
})
|
|
100
|
+
const maxValue = computed(() => {
|
|
101
|
+
const dynamicMax = maxField.value ? parseOptionalNumber(firstRow.value[maxField.value]) : undefined
|
|
102
|
+
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.max) ?? 100
|
|
103
|
+
})
|
|
42
104
|
const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
|
|
105
|
+
const fractionDigits = computed(() => Math.min([
|
|
106
|
+
value.value,
|
|
107
|
+
minValue.value,
|
|
108
|
+
maxValue.value,
|
|
109
|
+
].reduce((maxDigits, currentValue) => Math.max(maxDigits, countFractionDigits(currentValue)), 0), 3))
|
|
110
|
+
const shouldUseWholeNumbers = computed(() => Math.abs(maxValue.value) >= 1000)
|
|
111
|
+
const formattedValue = computed(() => formatChartValue(normalizeDisplayValue(value.value, shouldUseWholeNumbers.value), {
|
|
112
|
+
minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
113
|
+
maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
114
|
+
}))
|
|
115
|
+
const formattedMinValue = computed(() => formatChartValue(normalizeDisplayValue(minValue.value, shouldUseWholeNumbers.value), {
|
|
116
|
+
minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
117
|
+
maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
118
|
+
}))
|
|
119
|
+
const formattedMaxValue = computed(() => {
|
|
120
|
+
return formatChartValue(normalizeDisplayValue(maxValue.value, shouldUseWholeNumbers.value), {
|
|
121
|
+
minimumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
122
|
+
maximumFractionDigits: shouldUseWholeNumbers.value ? 0 : fractionDigits.value,
|
|
123
|
+
})
|
|
124
|
+
})
|
|
43
125
|
const progress = computed(() => {
|
|
44
126
|
const range = maxValue.value - minValue.value
|
|
45
127
|
return range > 0 ? Math.min(Math.max((value.value - minValue.value) / range, 0), 1) : 0
|
|
@@ -97,10 +179,10 @@ const gaugeColor = computed(() => gaugeConfig.value?.color || CHART_COLORS[0])
|
|
|
97
179
|
</svg>
|
|
98
180
|
|
|
99
181
|
<div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
|
|
100
|
-
{{
|
|
182
|
+
{{ formattedValue }}{{ gaugeConfig?.suffix ?? '' }}
|
|
101
183
|
</div>
|
|
102
184
|
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
103
|
-
{{
|
|
185
|
+
{{ formattedMinValue }} - {{ formattedMaxValue }}
|
|
104
186
|
</div>
|
|
105
187
|
</div>
|
|
106
188
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
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,
|
|
4
|
+
import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
|
|
5
5
|
import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
@@ -32,15 +32,37 @@ const pivotConfig = computed(() => props.widget.pivot_table as {
|
|
|
32
32
|
value_field?: string
|
|
33
33
|
aggregation?: 'count' | 'sum'
|
|
34
34
|
} | undefined)
|
|
35
|
-
const widgetData = computed(() => data.value?.data as
|
|
35
|
+
const widgetData = computed(() => data.value?.data as DashboardWidgetData | null)
|
|
36
36
|
const rows = computed(() => widgetData.value?.rows ?? [])
|
|
37
37
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
38
|
-
const
|
|
38
|
+
const isAggregateData = computed(() => widgetData.value?.kind === 'aggregate')
|
|
39
|
+
const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.column_field)
|
|
40
|
+
const rowField = computed(() => pivotConfig.value?.row_field || (isAggregateData.value ? 'group' : columns.value[0]))
|
|
39
41
|
const columnField = computed(() => pivotConfig.value?.column_field || columns.value[1])
|
|
40
|
-
const valueField = computed(() => pivotConfig.value?.value_field || columns.value[2])
|
|
42
|
+
const valueField = computed(() => pivotConfig.value?.value_field || columns.value[2] || columns.value[1])
|
|
41
43
|
const aggregation = computed(() => pivotConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
|
|
42
|
-
const pivotColumnLabels = computed(() =>
|
|
44
|
+
const pivotColumnLabels = computed(() => {
|
|
45
|
+
if (shouldRenderAggregateMatrix.value) {
|
|
46
|
+
return columns.value.filter((column) => column !== rowField.value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Array.from(new Set(rows.value.map((row) => formatChartLabel(row[columnField.value]))))
|
|
50
|
+
})
|
|
43
51
|
const pivotRows = computed(() => {
|
|
52
|
+
if (shouldRenderAggregateMatrix.value) {
|
|
53
|
+
return rows.value.map((row) => {
|
|
54
|
+
const item: Record<string, number | string> = {
|
|
55
|
+
label: formatChartLabel(row[rowField.value]),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const column of pivotColumnLabels.value) {
|
|
59
|
+
item[column] = toFiniteNumber(row[column])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return item
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
const rowMap = new Map<string, Record<string, number | string>>()
|
|
45
67
|
|
|
46
68
|
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 ?? props.widget.query?.limit ?? DEFAULT_PAGE_SIZE)
|
|
73
136
|
const dashboardSlugRef = computed(() => props.dashboardSlug)
|
|
74
137
|
const widgetIdRef = computed(() => props.widget.id)
|
|
138
|
+
const widgetDataRequest = computed(() => (
|
|
139
|
+
isPaginationEnabled.value
|
|
140
|
+
? {
|
|
141
|
+
pagination: {
|
|
142
|
+
page: currentPage.value,
|
|
143
|
+
pageSize: pageSize.value,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
: {}
|
|
147
|
+
))
|
|
75
148
|
const {
|
|
76
149
|
data,
|
|
77
150
|
isLoading,
|
|
151
|
+
isFetching,
|
|
78
152
|
error,
|
|
79
153
|
refetch,
|
|
80
|
-
} = useWidgetData(dashboardSlugRef, widgetIdRef)
|
|
154
|
+
} = useWidgetData(dashboardSlugRef, widgetIdRef, widgetDataRequest)
|
|
81
155
|
|
|
82
156
|
watch(
|
|
83
157
|
() => props.widget,
|
|
84
158
|
() => {
|
|
159
|
+
currentPage.value = 1
|
|
85
160
|
void refetch()
|
|
86
161
|
},
|
|
87
162
|
{ deep: true },
|
|
@@ -92,10 +167,47 @@ const tableData = computed(() => {
|
|
|
92
167
|
})
|
|
93
168
|
|
|
94
169
|
const columns = computed(() => {
|
|
95
|
-
const configuredColumns =
|
|
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 {};
|