@adminforth/dashboard 1.0.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/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +46 -0
- package/.woodpecker/release.yml +57 -0
- package/README.md +59 -0
- package/custom/api/dashboardApi.ts +213 -0
- package/custom/composables/useElementSize.ts +41 -0
- package/custom/model/dashboard.types.ts +73 -0
- package/custom/package.json +9 -0
- package/custom/pnpm-lock.yaml +24 -0
- package/custom/queries/useDashboardConfig.ts +51 -0
- package/custom/queries/useWidgetData.ts +51 -0
- package/custom/runtime/DashboardGroup.vue +185 -0
- package/custom/runtime/DashboardPage.vue +122 -0
- package/custom/runtime/DashboardRuntime.vue +435 -0
- package/custom/runtime/WidgetRenderer.vue +60 -0
- package/custom/runtime/WidgetShell.vue +152 -0
- package/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/custom/widgets/chart/ChartWidget.vue +188 -0
- package/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/custom/widgets/chart/chart.types.ts +34 -0
- package/custom/widgets/chart/chart.utils.ts +54 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/custom/widgets/chart/line/LineChart.vue +175 -0
- package/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/custom/widgets/registry.ts +51 -0
- package/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/custom/api/dashboardApi.d.ts +32 -0
- package/dist/custom/api/dashboardApi.js +179 -0
- package/dist/custom/api/dashboardApi.ts +213 -0
- package/dist/custom/composables/useElementSize.d.ts +8 -0
- package/dist/custom/composables/useElementSize.js +30 -0
- package/dist/custom/composables/useElementSize.ts +41 -0
- package/dist/custom/model/dashboard.types.d.ts +45 -0
- package/dist/custom/model/dashboard.types.js +14 -0
- package/dist/custom/model/dashboard.types.ts +73 -0
- package/dist/custom/package.json +9 -0
- package/dist/custom/pnpm-lock.yaml +24 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +112 -0
- package/dist/custom/queries/useDashboardConfig.js +57 -0
- package/dist/custom/queries/useDashboardConfig.ts +51 -0
- package/dist/custom/queries/useWidgetData.d.ts +90 -0
- package/dist/custom/queries/useWidgetData.js +57 -0
- package/dist/custom/queries/useWidgetData.ts +51 -0
- package/dist/custom/runtime/DashboardGroup.vue +185 -0
- package/dist/custom/runtime/DashboardPage.vue +122 -0
- package/dist/custom/runtime/DashboardRuntime.vue +435 -0
- package/dist/custom/runtime/WidgetRenderer.vue +60 -0
- package/dist/custom/runtime/WidgetShell.vue +152 -0
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/dist/custom/widgets/chart/ChartWidget.vue +188 -0
- package/dist/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/dist/custom/widgets/chart/chart.types.d.ts +25 -0
- package/dist/custom/widgets/chart/chart.types.js +2 -0
- package/dist/custom/widgets/chart/chart.types.ts +34 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +5 -0
- package/dist/custom/widgets/chart/chart.utils.js +52 -0
- package/dist/custom/widgets/chart/chart.utils.ts +54 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/dist/custom/widgets/chart/line/LineChart.vue +175 -0
- package/dist/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/dist/custom/widgets/registry.d.ts +11 -0
- package/dist/custom/widgets/registry.js +47 -0
- package/dist/custom/widgets/registry.ts +51 -0
- package/dist/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/endpoint/dashboard.d.ts +7 -0
- package/dist/endpoint/dashboard.js +29 -0
- package/dist/endpoint/groups.d.ts +30 -0
- package/dist/endpoint/groups.js +131 -0
- package/dist/endpoint/widgets.d.ts +15 -0
- package/dist/endpoint/widgets.js +182 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +124 -0
- package/dist/schema/api.d.ts +1205 -0
- package/dist/schema/api.js +84 -0
- package/dist/schema/widget.d.ts +514 -0
- package/dist/schema/widget.js +133 -0
- package/dist/services/dashboardConfigService.d.ts +35 -0
- package/dist/services/dashboardConfigService.js +79 -0
- package/dist/services/widgetConfigValidator.d.ts +8 -0
- package/dist/services/widgetConfigValidator.js +65 -0
- package/dist/services/widgetDataService.d.ts +20 -0
- package/dist/services/widgetDataService.js +32 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/endpoint/dashboard.ts +32 -0
- package/endpoint/groups.ts +213 -0
- package/endpoint/widgets.ts +255 -0
- package/index.ts +141 -0
- package/package.json +64 -0
- package/schema/api.ts +99 -0
- package/schema/widget.ts +159 -0
- package/services/dashboardConfigService.ts +136 -0
- package/services/widgetConfigValidator.ts +93 -0
- package/services/widgetDataService.ts +57 -0
- package/shims-vue.d.ts +5 -0
- package/tsconfig.json +18 -0
- package/types.ts +8 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const DashboardWidgetSizeSchema = z.enum([
|
|
3
|
+
'small',
|
|
4
|
+
'medium',
|
|
5
|
+
'large',
|
|
6
|
+
'wide',
|
|
7
|
+
'full',
|
|
8
|
+
]);
|
|
9
|
+
const WidgetBaseSchema = z.object({
|
|
10
|
+
id: z.string().optional(),
|
|
11
|
+
group_id: z.string().optional(),
|
|
12
|
+
label: z.string().optional(),
|
|
13
|
+
size: DashboardWidgetSizeSchema.optional(),
|
|
14
|
+
width: z.number().positive('Width must be greater than 0').optional(),
|
|
15
|
+
height: z.number().positive('Height must be greater than 0').optional(),
|
|
16
|
+
minWidth: z.number().nonnegative('Min width must be a non-negative number').optional(),
|
|
17
|
+
maxWidth: z.number().nonnegative('Max width must be a non-negative number').nullable().optional(),
|
|
18
|
+
order: z.number().optional(),
|
|
19
|
+
});
|
|
20
|
+
const ChartBaseSchema = z.object({
|
|
21
|
+
title: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
const ChartBucketSchema = z.object({
|
|
24
|
+
label: z.string().min(1, 'Bucket label is required'),
|
|
25
|
+
min: z.number().optional(),
|
|
26
|
+
max: z.number().optional(),
|
|
27
|
+
});
|
|
28
|
+
const ChartSeriesSchema = z.object({
|
|
29
|
+
name: z.string().min(1, 'Series name is required'),
|
|
30
|
+
field: z.string().min(1, 'Series field is required'),
|
|
31
|
+
color: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
const LineChartSchema = ChartBaseSchema.extend({
|
|
34
|
+
type: z.literal('line'),
|
|
35
|
+
x_field: z.string().optional(),
|
|
36
|
+
y_field: z.string().optional(),
|
|
37
|
+
series_name: z.string().optional(),
|
|
38
|
+
color: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
const BarChartSchema = ChartBaseSchema.extend({
|
|
41
|
+
type: z.literal('bar'),
|
|
42
|
+
label_field: z.string().optional(),
|
|
43
|
+
value_field: z.string().optional(),
|
|
44
|
+
bucket_field: z.string().optional(),
|
|
45
|
+
buckets: z.array(ChartBucketSchema).optional(),
|
|
46
|
+
color: z.string().optional(),
|
|
47
|
+
});
|
|
48
|
+
const StackedBarChartSchema = ChartBaseSchema.extend({
|
|
49
|
+
type: z.literal('stacked_bar'),
|
|
50
|
+
x_field: z.string().optional(),
|
|
51
|
+
series: z.array(ChartSeriesSchema).optional(),
|
|
52
|
+
colors: z.array(z.string()).optional(),
|
|
53
|
+
});
|
|
54
|
+
const PieChartSchema = ChartBaseSchema.extend({
|
|
55
|
+
type: z.literal('pie'),
|
|
56
|
+
label_field: z.string().optional(),
|
|
57
|
+
value_field: z.string().optional(),
|
|
58
|
+
colors: z.array(z.string()).optional(),
|
|
59
|
+
});
|
|
60
|
+
const HistogramChartSchema = ChartBaseSchema.extend({
|
|
61
|
+
type: z.literal('histogram'),
|
|
62
|
+
label_field: z.string().optional(),
|
|
63
|
+
value_field: z.string().optional(),
|
|
64
|
+
bucket_field: z.string().optional(),
|
|
65
|
+
buckets: z.array(ChartBucketSchema).optional(),
|
|
66
|
+
color: z.string().optional(),
|
|
67
|
+
});
|
|
68
|
+
const FunnelChartSchema = ChartBaseSchema.extend({
|
|
69
|
+
type: z.literal('funnel'),
|
|
70
|
+
label_field: z.string().optional(),
|
|
71
|
+
value_field: z.string().optional(),
|
|
72
|
+
colors: z.array(z.string()).optional(),
|
|
73
|
+
});
|
|
74
|
+
export const ChartConfigSchema = z.discriminatedUnion('type', [
|
|
75
|
+
LineChartSchema,
|
|
76
|
+
BarChartSchema,
|
|
77
|
+
StackedBarChartSchema,
|
|
78
|
+
PieChartSchema,
|
|
79
|
+
HistogramChartSchema,
|
|
80
|
+
FunnelChartSchema,
|
|
81
|
+
]);
|
|
82
|
+
export const DashboardWidgetQuerySchema = z.object({
|
|
83
|
+
resource: z.string().min(1, 'Query resource must be a non-empty string'),
|
|
84
|
+
select: z.array(z.string()).optional(),
|
|
85
|
+
order: z.object({
|
|
86
|
+
field: z.string().min(1, 'Order field is required'),
|
|
87
|
+
direction: z.enum(['asc', 'desc']),
|
|
88
|
+
}).optional(),
|
|
89
|
+
limit: z.number().optional(),
|
|
90
|
+
});
|
|
91
|
+
export const EmptyWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
92
|
+
target: z.literal('empty'),
|
|
93
|
+
});
|
|
94
|
+
const TableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
95
|
+
target: z.literal('table'),
|
|
96
|
+
table: z.unknown().optional(),
|
|
97
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
98
|
+
});
|
|
99
|
+
const ChartWidgetTargetConfigSchema = WidgetBaseSchema.extend({
|
|
100
|
+
target: z.literal('chart'),
|
|
101
|
+
chart: ChartConfigSchema,
|
|
102
|
+
query: DashboardWidgetQuerySchema,
|
|
103
|
+
});
|
|
104
|
+
const KpiCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
105
|
+
target: z.literal('kpi_card'),
|
|
106
|
+
kpi_card: z.unknown().optional(),
|
|
107
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
108
|
+
});
|
|
109
|
+
const GaugeCardWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
110
|
+
target: z.literal('gauge_card'),
|
|
111
|
+
gauge_card: z.unknown().optional(),
|
|
112
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
113
|
+
});
|
|
114
|
+
const PivotTableWidgetConfigSchema = WidgetBaseSchema.extend({
|
|
115
|
+
target: z.literal('pivot_table'),
|
|
116
|
+
pivot_table: z.unknown().optional(),
|
|
117
|
+
query: DashboardWidgetQuerySchema.optional(),
|
|
118
|
+
});
|
|
119
|
+
export const WidgetConfigSchema = z.discriminatedUnion('target', [
|
|
120
|
+
TableWidgetConfigSchema,
|
|
121
|
+
ChartWidgetTargetConfigSchema,
|
|
122
|
+
KpiCardWidgetConfigSchema,
|
|
123
|
+
GaugeCardWidgetConfigSchema,
|
|
124
|
+
PivotTableWidgetConfigSchema,
|
|
125
|
+
]);
|
|
126
|
+
export const StoredWidgetConfigSchema = z.discriminatedUnion('target', [
|
|
127
|
+
EmptyWidgetConfigSchema,
|
|
128
|
+
TableWidgetConfigSchema,
|
|
129
|
+
ChartWidgetTargetConfigSchema,
|
|
130
|
+
KpiCardWidgetConfigSchema,
|
|
131
|
+
GaugeCardWidgetConfigSchema,
|
|
132
|
+
PivotTableWidgetConfigSchema,
|
|
133
|
+
]);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IAdminForth } from 'adminforth';
|
|
2
|
+
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
export type DashboardRecord = {
|
|
4
|
+
id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
label: string;
|
|
7
|
+
revision: number;
|
|
8
|
+
config: unknown;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseStoredDashboardConfig(config: unknown): DashboardConfig;
|
|
11
|
+
export declare function buildDashboardResponse(dashboard: DashboardRecord): {
|
|
12
|
+
id: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
label: string;
|
|
15
|
+
revision: number;
|
|
16
|
+
config: DashboardConfig;
|
|
17
|
+
};
|
|
18
|
+
export type PersistedDashboardResponse = {
|
|
19
|
+
id: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
label: string;
|
|
22
|
+
revision: number;
|
|
23
|
+
config: DashboardConfig;
|
|
24
|
+
};
|
|
25
|
+
export declare function dashboardConfigUpdatedTopic(slug: string): string;
|
|
26
|
+
export declare function normalizeDashboardOrder(config: DashboardConfig): DashboardConfig;
|
|
27
|
+
export declare function getDashboardRecord(adminforth: IAdminForth, dashboardConfigsResourceId: string, slug: string): Promise<DashboardRecord | null>;
|
|
28
|
+
export declare function persistDashboardConfig(adminforth: IAdminForth, dashboardConfigsResourceId: string, dashboard: DashboardRecord, config: DashboardConfig): Promise<PersistedDashboardResponse>;
|
|
29
|
+
export type DashboardConfigService = {
|
|
30
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
31
|
+
parseStoredDashboardConfig: typeof parseStoredDashboardConfig;
|
|
32
|
+
persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
|
|
33
|
+
buildDashboardResponse: typeof buildDashboardResponse;
|
|
34
|
+
};
|
|
35
|
+
export declare function createDashboardConfigService(adminforth: IAdminForth, dashboardConfigsResourceId: string): DashboardConfigService;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Filters } from 'adminforth';
|
|
11
|
+
import { normalizeDashboardConfig } from '../custom/model/dashboard.types.js';
|
|
12
|
+
const DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX = '/opentopic/dashboard-config-updated';
|
|
13
|
+
export function parseStoredDashboardConfig(config) {
|
|
14
|
+
if (typeof config === 'string') {
|
|
15
|
+
return normalizeDashboardConfig(JSON.parse(config));
|
|
16
|
+
}
|
|
17
|
+
return normalizeDashboardConfig(config);
|
|
18
|
+
}
|
|
19
|
+
export function buildDashboardResponse(dashboard) {
|
|
20
|
+
return {
|
|
21
|
+
id: dashboard.id,
|
|
22
|
+
slug: dashboard.slug,
|
|
23
|
+
label: dashboard.label,
|
|
24
|
+
revision: dashboard.revision,
|
|
25
|
+
config: parseStoredDashboardConfig(dashboard.config),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function dashboardConfigUpdatedTopic(slug) {
|
|
29
|
+
return `${DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX}/${slug}`;
|
|
30
|
+
}
|
|
31
|
+
export function normalizeDashboardOrder(config) {
|
|
32
|
+
var _a;
|
|
33
|
+
const widgetsByGroupId = new Map();
|
|
34
|
+
for (const widget of config.widgets) {
|
|
35
|
+
const groupWidgets = (_a = widgetsByGroupId.get(widget.group_id)) !== null && _a !== void 0 ? _a : [];
|
|
36
|
+
groupWidgets.push(widget);
|
|
37
|
+
widgetsByGroupId.set(widget.group_id, groupWidgets);
|
|
38
|
+
}
|
|
39
|
+
for (const [groupId, widgets] of widgetsByGroupId.entries()) {
|
|
40
|
+
widgetsByGroupId.set(groupId, [...widgets].sort((a, b) => a.order - b.order));
|
|
41
|
+
}
|
|
42
|
+
return Object.assign(Object.assign({}, config), { groups: config.groups.map((group, index) => (Object.assign(Object.assign({}, group), { order: index + 1 }))), widgets: config.widgets.map((widget) => (Object.assign(Object.assign({}, widget), { order: widgetsByGroupId.get(widget.group_id).findIndex((item) => item.id === widget.id) + 1 }))) });
|
|
43
|
+
}
|
|
44
|
+
export function getDashboardRecord(adminforth, dashboardConfigsResourceId, slug) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const dashboardConfigs = adminforth.resource(dashboardConfigsResourceId);
|
|
47
|
+
const dashboard = yield dashboardConfigs.get(Filters.EQ('slug', slug));
|
|
48
|
+
return dashboard || null;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, config) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const normalizedConfig = normalizeDashboardOrder(config);
|
|
54
|
+
yield adminforth.resource(dashboardConfigsResourceId).update(dashboard.id, {
|
|
55
|
+
config: normalizedConfig,
|
|
56
|
+
revision: dashboard.revision + 1,
|
|
57
|
+
});
|
|
58
|
+
yield adminforth.websocket.publish(dashboardConfigUpdatedTopic(dashboard.slug), {
|
|
59
|
+
id: dashboard.id,
|
|
60
|
+
slug: dashboard.slug,
|
|
61
|
+
revision: dashboard.revision + 1,
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
id: dashboard.id,
|
|
65
|
+
slug: dashboard.slug,
|
|
66
|
+
label: dashboard.label,
|
|
67
|
+
revision: dashboard.revision + 1,
|
|
68
|
+
config: normalizedConfig,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function createDashboardConfigService(adminforth, dashboardConfigsResourceId) {
|
|
73
|
+
return {
|
|
74
|
+
getDashboardRecord: (slug) => getDashboardRecord(adminforth, dashboardConfigsResourceId, slug),
|
|
75
|
+
parseStoredDashboardConfig,
|
|
76
|
+
persistDashboardConfig: (dashboard, config) => persistDashboardConfig(adminforth, dashboardConfigsResourceId, dashboard, config),
|
|
77
|
+
buildDashboardResponse,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IAdminForth } from 'adminforth';
|
|
2
|
+
import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
4
|
+
export type WidgetConfigValidatorService = {
|
|
5
|
+
validateDashboardWidgetApiConfig: (widget: DashboardWidgetConfig) => DashboardWidgetConfigValidationError[];
|
|
6
|
+
};
|
|
7
|
+
export declare function validateDashboardWidgetApiConfig(adminforth: IAdminForth, widget: DashboardWidgetConfig): DashboardWidgetConfigValidationError[];
|
|
8
|
+
export declare function createWidgetConfigValidatorService(adminforth: IAdminForth): WidgetConfigValidatorService;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function validateDashboardWidgetApiConfig(adminforth, widget) {
|
|
2
|
+
var _a, _b;
|
|
3
|
+
if (widget.target !== 'chart') {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const errors = [];
|
|
7
|
+
if (!widget.query) {
|
|
8
|
+
errors.push({
|
|
9
|
+
field: 'query',
|
|
10
|
+
message: 'Chart widget must have query config',
|
|
11
|
+
});
|
|
12
|
+
return errors;
|
|
13
|
+
}
|
|
14
|
+
if (!widget.chart) {
|
|
15
|
+
errors.push({
|
|
16
|
+
field: 'chart',
|
|
17
|
+
message: 'Chart widget must have chart config',
|
|
18
|
+
});
|
|
19
|
+
return errors;
|
|
20
|
+
}
|
|
21
|
+
const query = widget.query;
|
|
22
|
+
const chart = widget.chart;
|
|
23
|
+
const resource = adminforth.config.resources.find((item) => item.resourceId === query.resource);
|
|
24
|
+
if (!resource) {
|
|
25
|
+
errors.push({
|
|
26
|
+
field: 'query.resource',
|
|
27
|
+
message: `Resource "${query.resource}" is not registered`,
|
|
28
|
+
});
|
|
29
|
+
return errors;
|
|
30
|
+
}
|
|
31
|
+
if (!query.select) {
|
|
32
|
+
return errors;
|
|
33
|
+
}
|
|
34
|
+
const resourceFields = resource.columns.map((column) => column.name);
|
|
35
|
+
for (const field of query.select) {
|
|
36
|
+
if (!resourceFields.includes(field)) {
|
|
37
|
+
errors.push({
|
|
38
|
+
field: 'query.select',
|
|
39
|
+
message: `Field "${field}" is not in resource "${query.resource}"`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const chartFields = [
|
|
44
|
+
chart.x_field,
|
|
45
|
+
chart.y_field,
|
|
46
|
+
chart.label_field,
|
|
47
|
+
chart.value_field,
|
|
48
|
+
chart.bucket_field,
|
|
49
|
+
...((_b = (_a = chart.series) === null || _a === void 0 ? void 0 : _a.map((series) => series.field)) !== null && _b !== void 0 ? _b : []),
|
|
50
|
+
].filter((field) => typeof field === 'string');
|
|
51
|
+
for (const field of chartFields) {
|
|
52
|
+
if (!query.select.includes(field)) {
|
|
53
|
+
errors.push({
|
|
54
|
+
field: 'query.select',
|
|
55
|
+
message: `Query select must include chart field "${field}"`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return errors;
|
|
60
|
+
}
|
|
61
|
+
export function createWidgetConfigValidatorService(adminforth) {
|
|
62
|
+
return {
|
|
63
|
+
validateDashboardWidgetApiConfig: (widget) => validateDashboardWidgetApiConfig(adminforth, widget),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IAdminForth } from 'adminforth';
|
|
2
|
+
import type { DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
export type DashboardWidgetQueryConfig = {
|
|
4
|
+
resource: string;
|
|
5
|
+
select?: string[];
|
|
6
|
+
order?: {
|
|
7
|
+
field: string;
|
|
8
|
+
direction: 'asc' | 'desc';
|
|
9
|
+
};
|
|
10
|
+
limit?: number;
|
|
11
|
+
};
|
|
12
|
+
export type DashboardWidgetData = {
|
|
13
|
+
columns: string[];
|
|
14
|
+
rows: Record<string, unknown>[];
|
|
15
|
+
};
|
|
16
|
+
export type WidgetDataService = {
|
|
17
|
+
getWidgetData: (widget: DashboardWidgetConfig) => Promise<DashboardWidgetData | null>;
|
|
18
|
+
};
|
|
19
|
+
export declare function getWidgetData(adminforth: IAdminForth, widget: DashboardWidgetConfig): Promise<DashboardWidgetData | null>;
|
|
20
|
+
export declare function createWidgetDataService(adminforth: IAdminForth): WidgetDataService;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Sorts } from 'adminforth';
|
|
11
|
+
export function getWidgetData(adminforth, widget) {
|
|
12
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
if (!widget.query) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const query = widget.query;
|
|
18
|
+
const rows = yield adminforth.resource(query.resource).list([], query.limit, 0, query.order
|
|
19
|
+
? [query.order.direction === 'desc' ? Sorts.DESC(query.order.field) : Sorts.ASC(query.order.field)]
|
|
20
|
+
: undefined);
|
|
21
|
+
const columns = (_a = query.select) !== null && _a !== void 0 ? _a : Object.keys((_b = rows[0]) !== null && _b !== void 0 ? _b : {});
|
|
22
|
+
return {
|
|
23
|
+
columns,
|
|
24
|
+
rows: rows.map((row) => (Object.fromEntries(columns.map((column) => [column, row[column]])))),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function createWidgetDataService(adminforth) {
|
|
29
|
+
return {
|
|
30
|
+
getWidgetData: (widget) => getWidgetData(adminforth, widget),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IHttpServer } from 'adminforth';
|
|
2
|
+
import { DashboardApiResponseSchema, SlugRequestSchema } from '../schema/api.js';
|
|
3
|
+
import type { DashboardRecord } from '../services/dashboardConfigService.js';
|
|
4
|
+
import { buildDashboardResponse } from '../services/dashboardConfigService.js';
|
|
5
|
+
|
|
6
|
+
type DashboardEndpointsContext = {
|
|
7
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function registerDashboardEndpoints(
|
|
11
|
+
server: IHttpServer,
|
|
12
|
+
ctx: DashboardEndpointsContext,
|
|
13
|
+
) {
|
|
14
|
+
server.endpoint({
|
|
15
|
+
method: 'POST',
|
|
16
|
+
path: '/dashboard/get-config',
|
|
17
|
+
description: 'Loads one dashboard configuration by slug for rendering or editing.',
|
|
18
|
+
request_schema: SlugRequestSchema,
|
|
19
|
+
response_schema: DashboardApiResponseSchema,
|
|
20
|
+
handler: async ({ body, response }) => {
|
|
21
|
+
const slug = String(body?.slug || 'default');
|
|
22
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
23
|
+
|
|
24
|
+
if (!dashboard) {
|
|
25
|
+
response.setStatus(404);
|
|
26
|
+
return { error: 'Dashboard not found' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return buildDashboardResponse(dashboard);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import type {
|
|
4
|
+
DashboardConfig,
|
|
5
|
+
DashboardGroupConfig,
|
|
6
|
+
} from '../custom/model/dashboard.types.js';
|
|
7
|
+
import {
|
|
8
|
+
DashboardApiResponseSchema,
|
|
9
|
+
GroupIdRequestSchema,
|
|
10
|
+
MoveGroupRequestSchema,
|
|
11
|
+
SetGroupConfigRequestSchema,
|
|
12
|
+
SlugRequestSchema,
|
|
13
|
+
} from '../schema/api.js';
|
|
14
|
+
|
|
15
|
+
type DashboardRecord = {
|
|
16
|
+
id: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
label: string;
|
|
19
|
+
revision: number;
|
|
20
|
+
config: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type GroupEndpointsContext = {
|
|
24
|
+
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
25
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
26
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
27
|
+
persistDashboardConfig: (
|
|
28
|
+
dashboard: DashboardRecord,
|
|
29
|
+
config: DashboardConfig,
|
|
30
|
+
) => Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
slug: string;
|
|
33
|
+
label: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
config: DashboardConfig;
|
|
36
|
+
}>;
|
|
37
|
+
buildDashboardResponse: (dashboard: DashboardRecord) => {
|
|
38
|
+
id: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
label: string;
|
|
41
|
+
revision: number;
|
|
42
|
+
config: DashboardConfig;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function registerGroupEndpoints(
|
|
47
|
+
server: IHttpServer,
|
|
48
|
+
ctx: GroupEndpointsContext,
|
|
49
|
+
) {
|
|
50
|
+
server.endpoint({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/dashboard/add_dashboard_group',
|
|
53
|
+
description: 'Adds a new group to a dashboard configuration. Superadmin only.',
|
|
54
|
+
request_schema: SlugRequestSchema,
|
|
55
|
+
response_schema: DashboardApiResponseSchema,
|
|
56
|
+
handler: async ({ body, adminUser, response }) => {
|
|
57
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
58
|
+
response.setStatus(403);
|
|
59
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const slug = String(body?.slug || 'default');
|
|
63
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
64
|
+
|
|
65
|
+
if (!dashboard) {
|
|
66
|
+
response.setStatus(404);
|
|
67
|
+
return { error: 'Dashboard not found' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
71
|
+
const nextOrder = config.groups.length + 1;
|
|
72
|
+
|
|
73
|
+
const group: DashboardGroupConfig = {
|
|
74
|
+
id: `group_${randomUUID()}`,
|
|
75
|
+
label: 'New group',
|
|
76
|
+
order: nextOrder,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
80
|
+
...config,
|
|
81
|
+
groups: [...config.groups, group],
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
server.endpoint({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
path: '/dashboard/set_dashboard_group_config',
|
|
89
|
+
description: 'Replaces editable JSON configuration for a dashboard group while preserving group id and order. Superadmin only.',
|
|
90
|
+
request_schema: SetGroupConfigRequestSchema,
|
|
91
|
+
response_schema: DashboardApiResponseSchema,
|
|
92
|
+
handler: async ({ body, adminUser, response }) => {
|
|
93
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
94
|
+
response.setStatus(403);
|
|
95
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const slug = String(body?.slug || 'default');
|
|
99
|
+
const groupId = String(body?.groupId || '');
|
|
100
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
101
|
+
|
|
102
|
+
if (!dashboard) {
|
|
103
|
+
response.setStatus(404);
|
|
104
|
+
return { error: 'Dashboard not found' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
108
|
+
const group = config.groups.find((item) => item.id === groupId);
|
|
109
|
+
|
|
110
|
+
if (!group) {
|
|
111
|
+
response.setStatus(404);
|
|
112
|
+
return { error: 'Dashboard group not found' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
116
|
+
...config,
|
|
117
|
+
groups: config.groups.map((item) => item.id === groupId
|
|
118
|
+
? {
|
|
119
|
+
...(body.config as DashboardGroupConfig),
|
|
120
|
+
id: group.id,
|
|
121
|
+
order: group.order,
|
|
122
|
+
}
|
|
123
|
+
: item),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
server.endpoint({
|
|
129
|
+
method: 'POST',
|
|
130
|
+
path: '/dashboard/move_dashboard_group',
|
|
131
|
+
description: 'Moves a dashboard group up or down in its dashboard. Superadmin only.',
|
|
132
|
+
request_schema: MoveGroupRequestSchema,
|
|
133
|
+
response_schema: DashboardApiResponseSchema,
|
|
134
|
+
handler: async ({ body, adminUser, response }) => {
|
|
135
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
136
|
+
response.setStatus(403);
|
|
137
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const slug = String(body?.slug || 'default');
|
|
141
|
+
const groupId = String(body?.groupId || '');
|
|
142
|
+
const direction = body?.direction === 'down' ? 'down' : 'up';
|
|
143
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
144
|
+
|
|
145
|
+
if (!dashboard) {
|
|
146
|
+
response.setStatus(404);
|
|
147
|
+
return { error: 'Dashboard not found' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
151
|
+
const sortedGroups = [...config.groups].sort((a, b) => a.order - b.order);
|
|
152
|
+
const currentIndex = sortedGroups.findIndex((group) => group.id === groupId);
|
|
153
|
+
|
|
154
|
+
if (currentIndex === -1) {
|
|
155
|
+
response.setStatus(404);
|
|
156
|
+
return { error: 'Dashboard group not found' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
160
|
+
|
|
161
|
+
if (targetIndex < 0 || targetIndex >= sortedGroups.length) {
|
|
162
|
+
return ctx.buildDashboardResponse(dashboard);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const reorderedGroups = [...sortedGroups];
|
|
166
|
+
const [group] = reorderedGroups.splice(currentIndex, 1);
|
|
167
|
+
reorderedGroups.splice(targetIndex, 0, group);
|
|
168
|
+
|
|
169
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
170
|
+
...config,
|
|
171
|
+
groups: reorderedGroups,
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
server.endpoint({
|
|
177
|
+
method: 'POST',
|
|
178
|
+
path: '/dashboard/remove_dashboard_group',
|
|
179
|
+
description: 'Removes a dashboard group and all widgets inside it. Superadmin only.',
|
|
180
|
+
request_schema: GroupIdRequestSchema,
|
|
181
|
+
response_schema: DashboardApiResponseSchema,
|
|
182
|
+
handler: async ({ body, adminUser, response }) => {
|
|
183
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
184
|
+
response.setStatus(403);
|
|
185
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const slug = String(body?.slug || 'default');
|
|
189
|
+
const groupId = String(body?.groupId || '');
|
|
190
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
191
|
+
|
|
192
|
+
if (!dashboard) {
|
|
193
|
+
response.setStatus(404);
|
|
194
|
+
return { error: 'Dashboard not found' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
198
|
+
const nextGroups = config.groups.filter((group) => group.id !== groupId);
|
|
199
|
+
|
|
200
|
+
if (nextGroups.length === config.groups.length) {
|
|
201
|
+
response.setStatus(404);
|
|
202
|
+
return { error: 'Dashboard group not found' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
206
|
+
...config,
|
|
207
|
+
groups: nextGroups,
|
|
208
|
+
widgets: config.widgets.filter((widget) => widget.group_id !== groupId),
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
}
|