@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,255 @@
|
|
|
1
|
+
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
4
|
+
import {
|
|
5
|
+
DashboardApiResponseSchema,
|
|
6
|
+
DashboardWidgetDataResponseSchema,
|
|
7
|
+
GroupIdRequestSchema,
|
|
8
|
+
MoveWidgetRequestSchema,
|
|
9
|
+
SetWidgetConfigRequestSchema,
|
|
10
|
+
WidgetIdRequestSchema,
|
|
11
|
+
} from '../schema/api.js';
|
|
12
|
+
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
13
|
+
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
14
|
+
|
|
15
|
+
type WidgetEndpointsContext = {
|
|
16
|
+
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
17
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
18
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
19
|
+
persistDashboardConfig: (
|
|
20
|
+
dashboard: DashboardRecord,
|
|
21
|
+
config: DashboardConfig,
|
|
22
|
+
) => Promise<PersistedDashboardResponse>;
|
|
23
|
+
buildDashboardResponse: (dashboard: DashboardRecord) => PersistedDashboardResponse;
|
|
24
|
+
validateDashboardWidgetApiConfig: (
|
|
25
|
+
widget: DashboardWidgetConfig,
|
|
26
|
+
) => DashboardWidgetConfigValidationError[];
|
|
27
|
+
getWidgetData: (widget: DashboardWidgetConfig) => Promise<unknown>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function registerWidgetEndpoints(
|
|
31
|
+
server: IHttpServer,
|
|
32
|
+
ctx: WidgetEndpointsContext,
|
|
33
|
+
) {
|
|
34
|
+
server.endpoint({
|
|
35
|
+
method: 'POST',
|
|
36
|
+
path: '/dashboard/add_dashboard_widget',
|
|
37
|
+
description: 'Adds a new empty widget to a dashboard group. Superadmin only.',
|
|
38
|
+
request_schema: GroupIdRequestSchema,
|
|
39
|
+
response_schema: DashboardApiResponseSchema,
|
|
40
|
+
handler: async ({ body, adminUser, response }) => {
|
|
41
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
42
|
+
response.setStatus(403);
|
|
43
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const slug = String(body?.slug || 'default');
|
|
47
|
+
const groupId = String(body?.groupId || '');
|
|
48
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
49
|
+
|
|
50
|
+
if (!dashboard) {
|
|
51
|
+
response.setStatus(404);
|
|
52
|
+
return { error: 'Dashboard not found' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
56
|
+
const group = config.groups.find((item) => item.id === groupId);
|
|
57
|
+
|
|
58
|
+
if (!group) {
|
|
59
|
+
response.setStatus(404);
|
|
60
|
+
return { error: 'Dashboard group not found' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nextOrder = config.widgets.filter((item) => item.group_id === groupId).length + 1;
|
|
64
|
+
const widget: DashboardWidgetConfig = {
|
|
65
|
+
id: `widget_${randomUUID()}`,
|
|
66
|
+
group_id: groupId,
|
|
67
|
+
label: 'New widget',
|
|
68
|
+
size: 'small',
|
|
69
|
+
order: nextOrder,
|
|
70
|
+
target: 'empty',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
74
|
+
...config,
|
|
75
|
+
widgets: [...config.widgets, widget],
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
server.endpoint({
|
|
81
|
+
method: 'POST',
|
|
82
|
+
path: '/dashboard/move_dashboard_widget',
|
|
83
|
+
description: 'Moves a dashboard widget up or down inside its group. Superadmin only.',
|
|
84
|
+
request_schema: MoveWidgetRequestSchema,
|
|
85
|
+
response_schema: DashboardApiResponseSchema,
|
|
86
|
+
handler: async ({ body, adminUser, response }) => {
|
|
87
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
88
|
+
response.setStatus(403);
|
|
89
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const slug = String(body?.slug || 'default');
|
|
93
|
+
const widgetId = String(body?.widgetId || '');
|
|
94
|
+
const direction = body?.direction === 'down' ? 'down' : 'up';
|
|
95
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
96
|
+
|
|
97
|
+
if (!dashboard) {
|
|
98
|
+
response.setStatus(404);
|
|
99
|
+
return { error: 'Dashboard not found' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
103
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
104
|
+
|
|
105
|
+
if (!widget) {
|
|
106
|
+
response.setStatus(404);
|
|
107
|
+
return { error: 'Dashboard widget not found' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sortedWidgets = config.widgets
|
|
111
|
+
.filter((item) => item.group_id === widget.group_id)
|
|
112
|
+
.sort((a, b) => a.order - b.order);
|
|
113
|
+
const currentIndex = sortedWidgets.findIndex((item) => item.id === widgetId);
|
|
114
|
+
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
115
|
+
|
|
116
|
+
if (targetIndex < 0 || targetIndex >= sortedWidgets.length) {
|
|
117
|
+
return ctx.buildDashboardResponse(dashboard);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const reorderedWidgets = [...sortedWidgets];
|
|
121
|
+
const [movedWidget] = reorderedWidgets.splice(currentIndex, 1);
|
|
122
|
+
reorderedWidgets.splice(targetIndex, 0, movedWidget);
|
|
123
|
+
const reorderedWidgetIds = new Map(reorderedWidgets.map((item, index) => [item.id, index + 1]));
|
|
124
|
+
|
|
125
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
126
|
+
...config,
|
|
127
|
+
widgets: config.widgets.map((item) => ({
|
|
128
|
+
...item,
|
|
129
|
+
order: reorderedWidgetIds.get(item.id) ?? item.order,
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
server.endpoint({
|
|
136
|
+
method: 'POST',
|
|
137
|
+
path: '/dashboard/remove_dashboard_widget',
|
|
138
|
+
description: 'Removes one dashboard widget by id. Superadmin only.',
|
|
139
|
+
request_schema: WidgetIdRequestSchema,
|
|
140
|
+
response_schema: DashboardApiResponseSchema,
|
|
141
|
+
handler: async ({ body, adminUser, response }) => {
|
|
142
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
143
|
+
response.setStatus(403);
|
|
144
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const slug = String(body?.slug || 'default');
|
|
148
|
+
const widgetId = String(body?.widgetId || '');
|
|
149
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
150
|
+
|
|
151
|
+
if (!dashboard) {
|
|
152
|
+
response.setStatus(404);
|
|
153
|
+
return { error: 'Dashboard not found' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
157
|
+
const nextWidgets = config.widgets.filter((item) => item.id !== widgetId);
|
|
158
|
+
|
|
159
|
+
if (nextWidgets.length === config.widgets.length) {
|
|
160
|
+
response.setStatus(404);
|
|
161
|
+
return { error: 'Dashboard widget not found' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
165
|
+
...config,
|
|
166
|
+
widgets: nextWidgets,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
server.endpoint({
|
|
172
|
+
method: 'POST',
|
|
173
|
+
path: '/dashboard/set_widget_config',
|
|
174
|
+
description: 'Replaces editable JSON configuration for a dashboard widget while preserving widget id, group id, and order. Superadmin only.',
|
|
175
|
+
request_schema: SetWidgetConfigRequestSchema,
|
|
176
|
+
response_schema: DashboardApiResponseSchema,
|
|
177
|
+
handler: async ({ body, adminUser, response }) => {
|
|
178
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
179
|
+
response.setStatus(403);
|
|
180
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const slug = String(body?.slug || 'default');
|
|
184
|
+
const widgetId = String(body?.widgetId || '');
|
|
185
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
186
|
+
|
|
187
|
+
if (!dashboard) {
|
|
188
|
+
response.setStatus(404);
|
|
189
|
+
return { error: 'Dashboard not found' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
193
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
194
|
+
|
|
195
|
+
if (!widget) {
|
|
196
|
+
response.setStatus(404);
|
|
197
|
+
return { error: 'Dashboard widget not found' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const typedWidgetConfig = body.config as DashboardWidgetConfig;
|
|
201
|
+
const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
|
|
202
|
+
|
|
203
|
+
if (apiValidationErrors.length) {
|
|
204
|
+
response.setStatus(422);
|
|
205
|
+
return {
|
|
206
|
+
error: 'Invalid widget config',
|
|
207
|
+
validationErrors: apiValidationErrors,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return ctx.persistDashboardConfig(dashboard, {
|
|
212
|
+
...config,
|
|
213
|
+
widgets: config.widgets.map((item) => item.id === widgetId
|
|
214
|
+
? {
|
|
215
|
+
...typedWidgetConfig,
|
|
216
|
+
id: widget.id,
|
|
217
|
+
group_id: widget.group_id,
|
|
218
|
+
order: widget.order,
|
|
219
|
+
}
|
|
220
|
+
: item),
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
server.endpoint({
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: '/dashboard/get_dashboard_widget_data',
|
|
228
|
+
description: 'Loads query result data for one dashboard widget by dashboard slug and widget id.',
|
|
229
|
+
request_schema: WidgetIdRequestSchema,
|
|
230
|
+
response_schema: DashboardWidgetDataResponseSchema,
|
|
231
|
+
handler: async ({ body, response }) => {
|
|
232
|
+
const slug = String(body?.slug || 'default');
|
|
233
|
+
const widgetId = String(body?.widgetId || '');
|
|
234
|
+
const dashboard = await ctx.getDashboardRecord(slug);
|
|
235
|
+
|
|
236
|
+
if (!dashboard) {
|
|
237
|
+
response.setStatus(404);
|
|
238
|
+
return { error: 'Dashboard not found' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
242
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
243
|
+
|
|
244
|
+
if (!widget) {
|
|
245
|
+
response.setStatus(404);
|
|
246
|
+
return { error: 'Dashboard widget not found' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
widget,
|
|
251
|
+
data: await ctx.getWidgetData(widget),
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AdminForthPlugin } from "adminforth";
|
|
2
|
+
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer } from "adminforth";
|
|
3
|
+
import type { PluginOptions } from "./types.js";
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { registerDashboardEndpoints } from './endpoint/dashboard.js';
|
|
7
|
+
import { registerGroupEndpoints } from "./endpoint/groups.js";
|
|
8
|
+
import { registerWidgetEndpoints } from './endpoint/widgets.js';
|
|
9
|
+
import { createDashboardConfigService } from "./services/dashboardConfigService.js";
|
|
10
|
+
import { createWidgetDataService } from "./services/widgetDataService.js";
|
|
11
|
+
import { createWidgetConfigValidatorService } from "./services/widgetConfigValidator.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DASHBOARD_CONFIG = {
|
|
14
|
+
version: 1,
|
|
15
|
+
groups: [{
|
|
16
|
+
id: 'default',
|
|
17
|
+
label: 'Default Group',
|
|
18
|
+
order: 1,
|
|
19
|
+
}],
|
|
20
|
+
widgets: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function canEditDashboard(adminUser: AdminUser) {
|
|
24
|
+
return adminUser.dbUser.role === 'superadmin';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default class DashboardPlugin extends AdminForthPlugin {
|
|
28
|
+
options: PluginOptions;
|
|
29
|
+
private didRegisterMenuProvider = false;
|
|
30
|
+
private didInstallSeedHook = false;
|
|
31
|
+
|
|
32
|
+
constructor(options: PluginOptions) {
|
|
33
|
+
super(options, import.meta.url);
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.customFolderName = 'custom';
|
|
36
|
+
this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
40
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
41
|
+
|
|
42
|
+
if (!this.didRegisterMenuProvider) {
|
|
43
|
+
this.didRegisterMenuProvider = true;
|
|
44
|
+
|
|
45
|
+
const adminforthWithDynamicMenu = adminforth as any;
|
|
46
|
+
adminforthWithDynamicMenu.registerMenuContributionProvider(async () => {
|
|
47
|
+
if (this.adminforth.statuses.dbDiscover !== 'done') {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const dashboards = await this.adminforth.resource(this.options.dashboardConfigsResourceId).list([]);
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
item: {
|
|
56
|
+
itemId: 'dashboardMenu',
|
|
57
|
+
type: 'group',
|
|
58
|
+
label: 'Dashboards',
|
|
59
|
+
icon: 'flowbite:chart-pie-solid',
|
|
60
|
+
open: true,
|
|
61
|
+
children: dashboards.map((dashboard: any) => ({
|
|
62
|
+
itemId: `dashboard-${dashboard.id}`,
|
|
63
|
+
type: 'page',
|
|
64
|
+
label: dashboard.label,
|
|
65
|
+
url: `/dashboard/${dashboard.slug}`,
|
|
66
|
+
})),
|
|
67
|
+
},
|
|
68
|
+
placement: { position: 'first' },
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!this.didInstallSeedHook) {
|
|
75
|
+
this.didInstallSeedHook = true;
|
|
76
|
+
|
|
77
|
+
const discoverDatabases = adminforth.discoverDatabases.bind(adminforth);
|
|
78
|
+
adminforth.discoverDatabases = async () => {
|
|
79
|
+
await discoverDatabases();
|
|
80
|
+
await this.ensureDefaultDashboardConfig();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
adminforth.config.customization.customPages.push({
|
|
85
|
+
path: '/dashboard/:slug',
|
|
86
|
+
component: {
|
|
87
|
+
file: this.componentPath('runtime/DashboardPage.vue'),
|
|
88
|
+
meta: {
|
|
89
|
+
title: 'Dashboard',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async ensureDefaultDashboardConfig() {
|
|
96
|
+
const dashboardConfigs = this.adminforth.resource(this.options.dashboardConfigsResourceId);
|
|
97
|
+
const dashboardsCount = await dashboardConfigs.count();
|
|
98
|
+
|
|
99
|
+
if (dashboardsCount > 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const createResult = await dashboardConfigs.create({
|
|
104
|
+
id: randomUUID(),
|
|
105
|
+
slug: 'default',
|
|
106
|
+
label: 'Default Dashboard',
|
|
107
|
+
revision: 1,
|
|
108
|
+
config: DEFAULT_DASHBOARD_CONFIG,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!createResult.ok) {
|
|
112
|
+
throw new Error(createResult.error || 'Failed to create default dashboard config');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setupEndpoints(server: IHttpServer) {
|
|
117
|
+
const dashboardConfigService = createDashboardConfigService(
|
|
118
|
+
this.adminforth,
|
|
119
|
+
this.options.dashboardConfigsResourceId,
|
|
120
|
+
);
|
|
121
|
+
const widgetDataService = createWidgetDataService(this.adminforth);
|
|
122
|
+
const widgetConfigValidatorService = createWidgetConfigValidatorService(this.adminforth);
|
|
123
|
+
|
|
124
|
+
const ctx = {
|
|
125
|
+
adminforth: this.adminforth,
|
|
126
|
+
dashboardConfigsResourceId: this.options.dashboardConfigsResourceId,
|
|
127
|
+
canEditDashboard,
|
|
128
|
+
...dashboardConfigService,
|
|
129
|
+
...widgetDataService,
|
|
130
|
+
...widgetConfigValidatorService,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
registerDashboardEndpoints(server, ctx);
|
|
134
|
+
registerGroupEndpoints(server, ctx);
|
|
135
|
+
registerWidgetEndpoints(server, ctx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
instanceUniqueRepresentation(): string {
|
|
139
|
+
return "dashboard";
|
|
140
|
+
}
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adminforth/dashboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"homepage": "https://adminforth.dev/docs/tutorial/Plugins/dashboard/",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"adminforth",
|
|
17
|
+
"dashboard"
|
|
18
|
+
],
|
|
19
|
+
"author": "DevForth (https://devforth.io)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"description": "Dashboard plugin for AdminForth",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "latest",
|
|
24
|
+
"adminforth": "^2.60.0",
|
|
25
|
+
"semantic-release": "^24.2.1",
|
|
26
|
+
"semantic-release-slack-bot": "^4.0.2",
|
|
27
|
+
"typescript": "^5.7.3",
|
|
28
|
+
"vue-tsc": "^3.3.2"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"adminforth": "^2.60.0"
|
|
32
|
+
},
|
|
33
|
+
"release": {
|
|
34
|
+
"plugins": [
|
|
35
|
+
"@semantic-release/commit-analyzer",
|
|
36
|
+
"@semantic-release/release-notes-generator",
|
|
37
|
+
"@semantic-release/npm",
|
|
38
|
+
"@semantic-release/github",
|
|
39
|
+
[
|
|
40
|
+
"semantic-release-slack-bot",
|
|
41
|
+
{
|
|
42
|
+
"packageName": "@adminforth/dashboard",
|
|
43
|
+
"notifyOnSuccess": true,
|
|
44
|
+
"notifyOnFail": true,
|
|
45
|
+
"slackIcon": ":package:",
|
|
46
|
+
"markdownReleaseNotes": true
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"branches": [
|
|
52
|
+
"main",
|
|
53
|
+
{
|
|
54
|
+
"name": "next",
|
|
55
|
+
"prerelease": true
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"vue": "^3.5.34",
|
|
60
|
+
"vue-router": "^5.0.7",
|
|
61
|
+
"yaml": "^2.9.0",
|
|
62
|
+
"zod": "^4.4.3"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/schema/api.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { toJSONSchema, z } from 'zod'
|
|
2
|
+
import { StoredWidgetConfigSchema, WidgetConfigSchema } from './widget.js'
|
|
3
|
+
|
|
4
|
+
function toAdminForthJsonSchema(schema: z.ZodType) {
|
|
5
|
+
return toJSONSchema(schema, { target: 'draft-7' })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DashboardErrorResponseZodSchema = z.object({
|
|
9
|
+
error: z.string(),
|
|
10
|
+
validationErrors: z.array(z.object({
|
|
11
|
+
field: z.string(),
|
|
12
|
+
message: z.string(),
|
|
13
|
+
})).optional(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const DashboardGroupZodSchema = z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
label: z.string(),
|
|
19
|
+
order: z.number(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const DashboardConfigZodSchema = z.object({
|
|
23
|
+
version: z.number(),
|
|
24
|
+
groups: z.array(DashboardGroupZodSchema),
|
|
25
|
+
widgets: z.array(StoredWidgetConfigSchema),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const DashboardResponseZodSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
slug: z.string(),
|
|
31
|
+
label: z.string(),
|
|
32
|
+
revision: z.number(),
|
|
33
|
+
config: DashboardConfigZodSchema,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const DashboardApiResponseZodSchema = z.union([
|
|
37
|
+
DashboardResponseZodSchema,
|
|
38
|
+
DashboardErrorResponseZodSchema,
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
export const DashboardWidgetDataResponseZodSchema = z.union([
|
|
42
|
+
z.object({
|
|
43
|
+
widget: StoredWidgetConfigSchema,
|
|
44
|
+
data: z.unknown(),
|
|
45
|
+
}),
|
|
46
|
+
DashboardErrorResponseZodSchema,
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
export const SlugRequestZodSchema = z.object({
|
|
50
|
+
slug: z.string().optional(),
|
|
51
|
+
}).strict()
|
|
52
|
+
|
|
53
|
+
export const GroupIdRequestZodSchema = z.object({
|
|
54
|
+
slug: z.string().optional(),
|
|
55
|
+
groupId: z.string(),
|
|
56
|
+
}).strict()
|
|
57
|
+
|
|
58
|
+
export const MoveGroupRequestZodSchema = z.object({
|
|
59
|
+
slug: z.string().optional(),
|
|
60
|
+
groupId: z.string(),
|
|
61
|
+
direction: z.enum(['up', 'down']),
|
|
62
|
+
}).strict()
|
|
63
|
+
|
|
64
|
+
export const SetGroupConfigRequestZodSchema = z.object({
|
|
65
|
+
slug: z.string().optional(),
|
|
66
|
+
groupId: z.string(),
|
|
67
|
+
config: DashboardGroupZodSchema,
|
|
68
|
+
}).strict()
|
|
69
|
+
|
|
70
|
+
export const WidgetIdRequestZodSchema = z.object({
|
|
71
|
+
slug: z.string().optional(),
|
|
72
|
+
widgetId: z.string(),
|
|
73
|
+
}).strict()
|
|
74
|
+
|
|
75
|
+
export const MoveWidgetRequestZodSchema = z.object({
|
|
76
|
+
slug: z.string().optional(),
|
|
77
|
+
widgetId: z.string(),
|
|
78
|
+
direction: z.enum(['up', 'down']),
|
|
79
|
+
}).strict()
|
|
80
|
+
|
|
81
|
+
export const SetWidgetConfigRequestZodSchema = z.object({
|
|
82
|
+
slug: z.string().optional(),
|
|
83
|
+
widgetId: z.string(),
|
|
84
|
+
config: WidgetConfigSchema,
|
|
85
|
+
}).strict()
|
|
86
|
+
|
|
87
|
+
export const DashboardErrorResponseSchema = toAdminForthJsonSchema(DashboardErrorResponseZodSchema)
|
|
88
|
+
export const DashboardGroupSchema = toAdminForthJsonSchema(DashboardGroupZodSchema)
|
|
89
|
+
export const DashboardConfigSchema = toAdminForthJsonSchema(DashboardConfigZodSchema)
|
|
90
|
+
export const DashboardResponseSchema = toAdminForthJsonSchema(DashboardResponseZodSchema)
|
|
91
|
+
export const DashboardApiResponseSchema = toAdminForthJsonSchema(DashboardApiResponseZodSchema)
|
|
92
|
+
export const DashboardWidgetDataResponseSchema = toAdminForthJsonSchema(DashboardWidgetDataResponseZodSchema)
|
|
93
|
+
export const SlugRequestSchema = toAdminForthJsonSchema(SlugRequestZodSchema)
|
|
94
|
+
export const GroupIdRequestSchema = toAdminForthJsonSchema(GroupIdRequestZodSchema)
|
|
95
|
+
export const MoveGroupRequestSchema = toAdminForthJsonSchema(MoveGroupRequestZodSchema)
|
|
96
|
+
export const SetGroupConfigRequestSchema = toAdminForthJsonSchema(SetGroupConfigRequestZodSchema)
|
|
97
|
+
export const WidgetIdRequestSchema = toAdminForthJsonSchema(WidgetIdRequestZodSchema)
|
|
98
|
+
export const MoveWidgetRequestSchema = toAdminForthJsonSchema(MoveWidgetRequestZodSchema)
|
|
99
|
+
export const SetWidgetConfigRequestSchema = toAdminForthJsonSchema(SetWidgetConfigRequestZodSchema)
|