@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,110 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full min-h-0 flex-col overflow-hidden rounded-lg border border-lightListBorder bg-lightTableBackground dark:border-darkListBorder dark:bg-darkTableBackground">
|
|
3
|
+
<div
|
|
4
|
+
v-if="isLoading"
|
|
5
|
+
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
6
|
+
>
|
|
7
|
+
Loading...
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-else-if="error"
|
|
12
|
+
class="p-4 text-sm text-lightInputErrorColor"
|
|
13
|
+
>
|
|
14
|
+
Failed to load table data
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else-if="!tableData?.rows.length"
|
|
19
|
+
class="p-4 text-sm text-lightListTableText dark:text-darkListTableText"
|
|
20
|
+
>
|
|
21
|
+
No data available
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
v-else
|
|
26
|
+
class="min-h-0 flex-1 overflow-auto"
|
|
27
|
+
>
|
|
28
|
+
<table class="min-w-max w-full border-collapse text-left text-sm">
|
|
29
|
+
<thead class="bg-lightTableHeadingBackground text-xs uppercase text-lightTableHeadingText dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
30
|
+
<tr>
|
|
31
|
+
<th
|
|
32
|
+
v-for="column in columns"
|
|
33
|
+
:key="column"
|
|
34
|
+
class="px-3 py-2 font-semibold"
|
|
35
|
+
>
|
|
36
|
+
{{ column }}
|
|
37
|
+
</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
|
|
41
|
+
<tbody>
|
|
42
|
+
<tr
|
|
43
|
+
v-for="(row, index) in tableData.rows"
|
|
44
|
+
:key="index"
|
|
45
|
+
class="border-t border-lightListBorder odd:bg-lightTableOddBackground even:bg-lightTableEvenBackground dark:border-darkListBorder odd:dark:bg-darkTableOddBackground even:dark:bg-darkTableEvenBackground"
|
|
46
|
+
>
|
|
47
|
+
<td
|
|
48
|
+
v-for="column in columns"
|
|
49
|
+
:key="column"
|
|
50
|
+
class="px-3 py-2 text-lightListTableText dark:text-darkListTableText"
|
|
51
|
+
>
|
|
52
|
+
{{ formatCell(row[column]) }}
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import { computed, watch } from 'vue'
|
|
65
|
+
import { useWidgetData } from '../../queries/useWidgetData.js'
|
|
66
|
+
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
|
|
67
|
+
|
|
68
|
+
const props = defineProps<{
|
|
69
|
+
dashboardSlug: string
|
|
70
|
+
widget: DashboardWidgetConfig
|
|
71
|
+
}>()
|
|
72
|
+
|
|
73
|
+
const dashboardSlugRef = computed(() => props.dashboardSlug)
|
|
74
|
+
const widgetIdRef = computed(() => props.widget.id)
|
|
75
|
+
const {
|
|
76
|
+
data,
|
|
77
|
+
isLoading,
|
|
78
|
+
error,
|
|
79
|
+
refetch,
|
|
80
|
+
} = useWidgetData(dashboardSlugRef, widgetIdRef)
|
|
81
|
+
|
|
82
|
+
watch(
|
|
83
|
+
() => props.widget,
|
|
84
|
+
() => {
|
|
85
|
+
void refetch()
|
|
86
|
+
},
|
|
87
|
+
{ deep: true },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const tableData = computed(() => {
|
|
91
|
+
return data.value?.data as DashboardWidgetTableData | null
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const columns = computed(() => {
|
|
95
|
+
const configuredColumns = (props.widget.table as { columns?: string[] } | undefined)?.columns
|
|
96
|
+
return configuredColumns ?? tableData.value?.columns ?? []
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function formatCell(value: unknown) {
|
|
100
|
+
if (value === null || value === undefined) {
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value === 'object') {
|
|
105
|
+
return JSON.stringify(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return String(value)
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IHttpServer } from 'adminforth';
|
|
2
|
+
import type { DashboardRecord } from '../services/dashboardConfigService.js';
|
|
3
|
+
type DashboardEndpointsContext = {
|
|
4
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
5
|
+
};
|
|
6
|
+
export declare function registerDashboardEndpoints(server: IHttpServer, ctx: DashboardEndpointsContext): void;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { DashboardApiResponseSchema, SlugRequestSchema } from '../schema/api.js';
|
|
11
|
+
import { buildDashboardResponse } from '../services/dashboardConfigService.js';
|
|
12
|
+
export function registerDashboardEndpoints(server, ctx) {
|
|
13
|
+
server.endpoint({
|
|
14
|
+
method: 'POST',
|
|
15
|
+
path: '/dashboard/get-config',
|
|
16
|
+
description: 'Loads one dashboard configuration by slug for rendering or editing.',
|
|
17
|
+
request_schema: SlugRequestSchema,
|
|
18
|
+
response_schema: DashboardApiResponseSchema,
|
|
19
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
|
|
20
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
21
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
22
|
+
if (!dashboard) {
|
|
23
|
+
response.setStatus(404);
|
|
24
|
+
return { error: 'Dashboard not found' };
|
|
25
|
+
}
|
|
26
|
+
return buildDashboardResponse(dashboard);
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
+
import type { DashboardConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
type DashboardRecord = {
|
|
4
|
+
id: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
label: string;
|
|
7
|
+
revision: number;
|
|
8
|
+
config: unknown;
|
|
9
|
+
};
|
|
10
|
+
type GroupEndpointsContext = {
|
|
11
|
+
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
12
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
13
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
14
|
+
persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<{
|
|
15
|
+
id: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
label: string;
|
|
18
|
+
revision: number;
|
|
19
|
+
config: DashboardConfig;
|
|
20
|
+
}>;
|
|
21
|
+
buildDashboardResponse: (dashboard: DashboardRecord) => {
|
|
22
|
+
id: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
label: string;
|
|
25
|
+
revision: number;
|
|
26
|
+
config: DashboardConfig;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function registerGroupEndpoints(server: IHttpServer, ctx: GroupEndpointsContext): void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { randomUUID } from 'crypto';
|
|
11
|
+
import { DashboardApiResponseSchema, GroupIdRequestSchema, MoveGroupRequestSchema, SetGroupConfigRequestSchema, SlugRequestSchema, } from '../schema/api.js';
|
|
12
|
+
export function registerGroupEndpoints(server, ctx) {
|
|
13
|
+
server.endpoint({
|
|
14
|
+
method: 'POST',
|
|
15
|
+
path: '/dashboard/add_dashboard_group',
|
|
16
|
+
description: 'Adds a new group to a dashboard configuration. Superadmin only.',
|
|
17
|
+
request_schema: SlugRequestSchema,
|
|
18
|
+
response_schema: DashboardApiResponseSchema,
|
|
19
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
20
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
21
|
+
response.setStatus(403);
|
|
22
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
23
|
+
}
|
|
24
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
25
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
26
|
+
if (!dashboard) {
|
|
27
|
+
response.setStatus(404);
|
|
28
|
+
return { error: 'Dashboard not found' };
|
|
29
|
+
}
|
|
30
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
31
|
+
const nextOrder = config.groups.length + 1;
|
|
32
|
+
const group = {
|
|
33
|
+
id: `group_${randomUUID()}`,
|
|
34
|
+
label: 'New group',
|
|
35
|
+
order: nextOrder,
|
|
36
|
+
};
|
|
37
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { groups: [...config.groups, group] }));
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
server.endpoint({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/dashboard/set_dashboard_group_config',
|
|
43
|
+
description: 'Replaces editable JSON configuration for a dashboard group while preserving group id and order. Superadmin only.',
|
|
44
|
+
request_schema: SetGroupConfigRequestSchema,
|
|
45
|
+
response_schema: DashboardApiResponseSchema,
|
|
46
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
47
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
48
|
+
response.setStatus(403);
|
|
49
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
50
|
+
}
|
|
51
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
52
|
+
const groupId = String((body === null || body === void 0 ? void 0 : body.groupId) || '');
|
|
53
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
54
|
+
if (!dashboard) {
|
|
55
|
+
response.setStatus(404);
|
|
56
|
+
return { error: 'Dashboard not found' };
|
|
57
|
+
}
|
|
58
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
59
|
+
const group = config.groups.find((item) => item.id === groupId);
|
|
60
|
+
if (!group) {
|
|
61
|
+
response.setStatus(404);
|
|
62
|
+
return { error: 'Dashboard group not found' };
|
|
63
|
+
}
|
|
64
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { groups: config.groups.map((item) => item.id === groupId
|
|
65
|
+
? Object.assign(Object.assign({}, body.config), { id: group.id, order: group.order }) : item) }));
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
server.endpoint({
|
|
69
|
+
method: 'POST',
|
|
70
|
+
path: '/dashboard/move_dashboard_group',
|
|
71
|
+
description: 'Moves a dashboard group up or down in its dashboard. Superadmin only.',
|
|
72
|
+
request_schema: MoveGroupRequestSchema,
|
|
73
|
+
response_schema: DashboardApiResponseSchema,
|
|
74
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
75
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
76
|
+
response.setStatus(403);
|
|
77
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
78
|
+
}
|
|
79
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
80
|
+
const groupId = String((body === null || body === void 0 ? void 0 : body.groupId) || '');
|
|
81
|
+
const direction = (body === null || body === void 0 ? void 0 : body.direction) === 'down' ? 'down' : 'up';
|
|
82
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
83
|
+
if (!dashboard) {
|
|
84
|
+
response.setStatus(404);
|
|
85
|
+
return { error: 'Dashboard not found' };
|
|
86
|
+
}
|
|
87
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
88
|
+
const sortedGroups = [...config.groups].sort((a, b) => a.order - b.order);
|
|
89
|
+
const currentIndex = sortedGroups.findIndex((group) => group.id === groupId);
|
|
90
|
+
if (currentIndex === -1) {
|
|
91
|
+
response.setStatus(404);
|
|
92
|
+
return { error: 'Dashboard group not found' };
|
|
93
|
+
}
|
|
94
|
+
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
95
|
+
if (targetIndex < 0 || targetIndex >= sortedGroups.length) {
|
|
96
|
+
return ctx.buildDashboardResponse(dashboard);
|
|
97
|
+
}
|
|
98
|
+
const reorderedGroups = [...sortedGroups];
|
|
99
|
+
const [group] = reorderedGroups.splice(currentIndex, 1);
|
|
100
|
+
reorderedGroups.splice(targetIndex, 0, group);
|
|
101
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { groups: reorderedGroups }));
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
server.endpoint({
|
|
105
|
+
method: 'POST',
|
|
106
|
+
path: '/dashboard/remove_dashboard_group',
|
|
107
|
+
description: 'Removes a dashboard group and all widgets inside it. Superadmin only.',
|
|
108
|
+
request_schema: GroupIdRequestSchema,
|
|
109
|
+
response_schema: DashboardApiResponseSchema,
|
|
110
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
111
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
112
|
+
response.setStatus(403);
|
|
113
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
114
|
+
}
|
|
115
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
116
|
+
const groupId = String((body === null || body === void 0 ? void 0 : body.groupId) || '');
|
|
117
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
118
|
+
if (!dashboard) {
|
|
119
|
+
response.setStatus(404);
|
|
120
|
+
return { error: 'Dashboard not found' };
|
|
121
|
+
}
|
|
122
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
123
|
+
const nextGroups = config.groups.filter((group) => group.id !== groupId);
|
|
124
|
+
if (nextGroups.length === config.groups.length) {
|
|
125
|
+
response.setStatus(404);
|
|
126
|
+
return { error: 'Dashboard group not found' };
|
|
127
|
+
}
|
|
128
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { groups: nextGroups, widgets: config.widgets.filter((widget) => widget.group_id !== groupId) }));
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AdminUser, IHttpServer } from 'adminforth';
|
|
2
|
+
import type { DashboardConfig, DashboardWidgetConfig } from '../custom/model/dashboard.types.js';
|
|
3
|
+
import type { DashboardWidgetConfigValidationError } from '../schema/widget.js';
|
|
4
|
+
import type { DashboardRecord, PersistedDashboardResponse } from '../services/dashboardConfigService.js';
|
|
5
|
+
type WidgetEndpointsContext = {
|
|
6
|
+
canEditDashboard: (adminUser: AdminUser) => boolean;
|
|
7
|
+
getDashboardRecord: (slug: string) => Promise<DashboardRecord | null>;
|
|
8
|
+
parseStoredDashboardConfig: (config: unknown) => DashboardConfig;
|
|
9
|
+
persistDashboardConfig: (dashboard: DashboardRecord, config: DashboardConfig) => Promise<PersistedDashboardResponse>;
|
|
10
|
+
buildDashboardResponse: (dashboard: DashboardRecord) => PersistedDashboardResponse;
|
|
11
|
+
validateDashboardWidgetApiConfig: (widget: DashboardWidgetConfig) => DashboardWidgetConfigValidationError[];
|
|
12
|
+
getWidgetData: (widget: DashboardWidgetConfig) => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
export declare function registerWidgetEndpoints(server: IHttpServer, ctx: WidgetEndpointsContext): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { randomUUID } from 'crypto';
|
|
11
|
+
import { DashboardApiResponseSchema, DashboardWidgetDataResponseSchema, GroupIdRequestSchema, MoveWidgetRequestSchema, SetWidgetConfigRequestSchema, WidgetIdRequestSchema, } from '../schema/api.js';
|
|
12
|
+
export function registerWidgetEndpoints(server, ctx) {
|
|
13
|
+
server.endpoint({
|
|
14
|
+
method: 'POST',
|
|
15
|
+
path: '/dashboard/add_dashboard_widget',
|
|
16
|
+
description: 'Adds a new empty widget to a dashboard group. Superadmin only.',
|
|
17
|
+
request_schema: GroupIdRequestSchema,
|
|
18
|
+
response_schema: DashboardApiResponseSchema,
|
|
19
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
20
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
21
|
+
response.setStatus(403);
|
|
22
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
23
|
+
}
|
|
24
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
25
|
+
const groupId = String((body === null || body === void 0 ? void 0 : body.groupId) || '');
|
|
26
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
27
|
+
if (!dashboard) {
|
|
28
|
+
response.setStatus(404);
|
|
29
|
+
return { error: 'Dashboard not found' };
|
|
30
|
+
}
|
|
31
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
32
|
+
const group = config.groups.find((item) => item.id === groupId);
|
|
33
|
+
if (!group) {
|
|
34
|
+
response.setStatus(404);
|
|
35
|
+
return { error: 'Dashboard group not found' };
|
|
36
|
+
}
|
|
37
|
+
const nextOrder = config.widgets.filter((item) => item.group_id === groupId).length + 1;
|
|
38
|
+
const widget = {
|
|
39
|
+
id: `widget_${randomUUID()}`,
|
|
40
|
+
group_id: groupId,
|
|
41
|
+
label: 'New widget',
|
|
42
|
+
size: 'small',
|
|
43
|
+
order: nextOrder,
|
|
44
|
+
target: 'empty',
|
|
45
|
+
};
|
|
46
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { widgets: [...config.widgets, widget] }));
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
server.endpoint({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/dashboard/move_dashboard_widget',
|
|
52
|
+
description: 'Moves a dashboard widget up or down inside its group. Superadmin only.',
|
|
53
|
+
request_schema: MoveWidgetRequestSchema,
|
|
54
|
+
response_schema: DashboardApiResponseSchema,
|
|
55
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
56
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
57
|
+
response.setStatus(403);
|
|
58
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
59
|
+
}
|
|
60
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
61
|
+
const widgetId = String((body === null || body === void 0 ? void 0 : body.widgetId) || '');
|
|
62
|
+
const direction = (body === null || body === void 0 ? void 0 : body.direction) === 'down' ? 'down' : 'up';
|
|
63
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
64
|
+
if (!dashboard) {
|
|
65
|
+
response.setStatus(404);
|
|
66
|
+
return { error: 'Dashboard not found' };
|
|
67
|
+
}
|
|
68
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
69
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
70
|
+
if (!widget) {
|
|
71
|
+
response.setStatus(404);
|
|
72
|
+
return { error: 'Dashboard widget not found' };
|
|
73
|
+
}
|
|
74
|
+
const sortedWidgets = config.widgets
|
|
75
|
+
.filter((item) => item.group_id === widget.group_id)
|
|
76
|
+
.sort((a, b) => a.order - b.order);
|
|
77
|
+
const currentIndex = sortedWidgets.findIndex((item) => item.id === widgetId);
|
|
78
|
+
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
|
79
|
+
if (targetIndex < 0 || targetIndex >= sortedWidgets.length) {
|
|
80
|
+
return ctx.buildDashboardResponse(dashboard);
|
|
81
|
+
}
|
|
82
|
+
const reorderedWidgets = [...sortedWidgets];
|
|
83
|
+
const [movedWidget] = reorderedWidgets.splice(currentIndex, 1);
|
|
84
|
+
reorderedWidgets.splice(targetIndex, 0, movedWidget);
|
|
85
|
+
const reorderedWidgetIds = new Map(reorderedWidgets.map((item, index) => [item.id, index + 1]));
|
|
86
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { widgets: config.widgets.map((item) => {
|
|
87
|
+
var _a;
|
|
88
|
+
return (Object.assign(Object.assign({}, item), { order: (_a = reorderedWidgetIds.get(item.id)) !== null && _a !== void 0 ? _a : item.order }));
|
|
89
|
+
}) }));
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
server.endpoint({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
path: '/dashboard/remove_dashboard_widget',
|
|
95
|
+
description: 'Removes one dashboard widget by id. Superadmin only.',
|
|
96
|
+
request_schema: WidgetIdRequestSchema,
|
|
97
|
+
response_schema: DashboardApiResponseSchema,
|
|
98
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
99
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
100
|
+
response.setStatus(403);
|
|
101
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
102
|
+
}
|
|
103
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
104
|
+
const widgetId = String((body === null || body === void 0 ? void 0 : body.widgetId) || '');
|
|
105
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
106
|
+
if (!dashboard) {
|
|
107
|
+
response.setStatus(404);
|
|
108
|
+
return { error: 'Dashboard not found' };
|
|
109
|
+
}
|
|
110
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
111
|
+
const nextWidgets = config.widgets.filter((item) => item.id !== widgetId);
|
|
112
|
+
if (nextWidgets.length === config.widgets.length) {
|
|
113
|
+
response.setStatus(404);
|
|
114
|
+
return { error: 'Dashboard widget not found' };
|
|
115
|
+
}
|
|
116
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { widgets: nextWidgets }));
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
server.endpoint({
|
|
120
|
+
method: 'POST',
|
|
121
|
+
path: '/dashboard/set_widget_config',
|
|
122
|
+
description: 'Replaces editable JSON configuration for a dashboard widget while preserving widget id, group id, and order. Superadmin only.',
|
|
123
|
+
request_schema: SetWidgetConfigRequestSchema,
|
|
124
|
+
response_schema: DashboardApiResponseSchema,
|
|
125
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
126
|
+
if (!ctx.canEditDashboard(adminUser)) {
|
|
127
|
+
response.setStatus(403);
|
|
128
|
+
return { error: 'Dashboard edit is not allowed' };
|
|
129
|
+
}
|
|
130
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
131
|
+
const widgetId = String((body === null || body === void 0 ? void 0 : body.widgetId) || '');
|
|
132
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
133
|
+
if (!dashboard) {
|
|
134
|
+
response.setStatus(404);
|
|
135
|
+
return { error: 'Dashboard not found' };
|
|
136
|
+
}
|
|
137
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
138
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
139
|
+
if (!widget) {
|
|
140
|
+
response.setStatus(404);
|
|
141
|
+
return { error: 'Dashboard widget not found' };
|
|
142
|
+
}
|
|
143
|
+
const typedWidgetConfig = body.config;
|
|
144
|
+
const apiValidationErrors = ctx.validateDashboardWidgetApiConfig(typedWidgetConfig);
|
|
145
|
+
if (apiValidationErrors.length) {
|
|
146
|
+
response.setStatus(422);
|
|
147
|
+
return {
|
|
148
|
+
error: 'Invalid widget config',
|
|
149
|
+
validationErrors: apiValidationErrors,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return ctx.persistDashboardConfig(dashboard, Object.assign(Object.assign({}, config), { widgets: config.widgets.map((item) => item.id === widgetId
|
|
153
|
+
? Object.assign(Object.assign({}, typedWidgetConfig), { id: widget.id, group_id: widget.group_id, order: widget.order }) : item) }));
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
server.endpoint({
|
|
157
|
+
method: 'POST',
|
|
158
|
+
path: '/dashboard/get_dashboard_widget_data',
|
|
159
|
+
description: 'Loads query result data for one dashboard widget by dashboard slug and widget id.',
|
|
160
|
+
request_schema: WidgetIdRequestSchema,
|
|
161
|
+
response_schema: DashboardWidgetDataResponseSchema,
|
|
162
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response }) {
|
|
163
|
+
const slug = String((body === null || body === void 0 ? void 0 : body.slug) || 'default');
|
|
164
|
+
const widgetId = String((body === null || body === void 0 ? void 0 : body.widgetId) || '');
|
|
165
|
+
const dashboard = yield ctx.getDashboardRecord(slug);
|
|
166
|
+
if (!dashboard) {
|
|
167
|
+
response.setStatus(404);
|
|
168
|
+
return { error: 'Dashboard not found' };
|
|
169
|
+
}
|
|
170
|
+
const config = ctx.parseStoredDashboardConfig(dashboard.config);
|
|
171
|
+
const widget = config.widgets.find((item) => item.id === widgetId);
|
|
172
|
+
if (!widget) {
|
|
173
|
+
response.setStatus(404);
|
|
174
|
+
return { error: 'Dashboard widget not found' };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
widget,
|
|
178
|
+
data: yield ctx.getWidgetData(widget),
|
|
179
|
+
};
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AdminForthPlugin } from "adminforth";
|
|
2
|
+
import type { AdminForthResource, IAdminForth, IHttpServer } from "adminforth";
|
|
3
|
+
import type { PluginOptions } from "./types.js";
|
|
4
|
+
export default class DashboardPlugin extends AdminForthPlugin {
|
|
5
|
+
options: PluginOptions;
|
|
6
|
+
private didRegisterMenuProvider;
|
|
7
|
+
private didInstallSeedHook;
|
|
8
|
+
constructor(options: PluginOptions);
|
|
9
|
+
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
|
|
10
|
+
private ensureDefaultDashboardConfig;
|
|
11
|
+
setupEndpoints(server: IHttpServer): void;
|
|
12
|
+
instanceUniqueRepresentation(): string;
|
|
13
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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 { AdminForthPlugin } from "adminforth";
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { registerDashboardEndpoints } from './endpoint/dashboard.js';
|
|
14
|
+
import { registerGroupEndpoints } from "./endpoint/groups.js";
|
|
15
|
+
import { registerWidgetEndpoints } from './endpoint/widgets.js';
|
|
16
|
+
import { createDashboardConfigService } from "./services/dashboardConfigService.js";
|
|
17
|
+
import { createWidgetDataService } from "./services/widgetDataService.js";
|
|
18
|
+
import { createWidgetConfigValidatorService } from "./services/widgetConfigValidator.js";
|
|
19
|
+
const DEFAULT_DASHBOARD_CONFIG = {
|
|
20
|
+
version: 1,
|
|
21
|
+
groups: [{
|
|
22
|
+
id: 'default',
|
|
23
|
+
label: 'Default Group',
|
|
24
|
+
order: 1,
|
|
25
|
+
}],
|
|
26
|
+
widgets: [],
|
|
27
|
+
};
|
|
28
|
+
function canEditDashboard(adminUser) {
|
|
29
|
+
return adminUser.dbUser.role === 'superadmin';
|
|
30
|
+
}
|
|
31
|
+
export default class DashboardPlugin extends AdminForthPlugin {
|
|
32
|
+
constructor(options) {
|
|
33
|
+
super(options, import.meta.url);
|
|
34
|
+
this.didRegisterMenuProvider = false;
|
|
35
|
+
this.didInstallSeedHook = false;
|
|
36
|
+
this.options = options;
|
|
37
|
+
this.customFolderName = 'custom';
|
|
38
|
+
this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
|
|
39
|
+
}
|
|
40
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
41
|
+
const _super = Object.create(null, {
|
|
42
|
+
modifyResourceConfig: { get: () => super.modifyResourceConfig }
|
|
43
|
+
});
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
46
|
+
if (!this.didRegisterMenuProvider) {
|
|
47
|
+
this.didRegisterMenuProvider = true;
|
|
48
|
+
const adminforthWithDynamicMenu = adminforth;
|
|
49
|
+
adminforthWithDynamicMenu.registerMenuContributionProvider(() => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
if (this.adminforth.statuses.dbDiscover !== 'done') {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const dashboards = yield this.adminforth.resource(this.options.dashboardConfigsResourceId).list([]);
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
item: {
|
|
57
|
+
itemId: 'dashboardMenu',
|
|
58
|
+
type: 'group',
|
|
59
|
+
label: 'Dashboards',
|
|
60
|
+
icon: 'flowbite:chart-pie-solid',
|
|
61
|
+
open: true,
|
|
62
|
+
children: dashboards.map((dashboard) => ({
|
|
63
|
+
itemId: `dashboard-${dashboard.id}`,
|
|
64
|
+
type: 'page',
|
|
65
|
+
label: dashboard.label,
|
|
66
|
+
url: `/dashboard/${dashboard.slug}`,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
placement: { position: 'first' },
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
if (!this.didInstallSeedHook) {
|
|
75
|
+
this.didInstallSeedHook = true;
|
|
76
|
+
const discoverDatabases = adminforth.discoverDatabases.bind(adminforth);
|
|
77
|
+
adminforth.discoverDatabases = () => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
yield discoverDatabases();
|
|
79
|
+
yield this.ensureDefaultDashboardConfig();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
adminforth.config.customization.customPages.push({
|
|
83
|
+
path: '/dashboard/:slug',
|
|
84
|
+
component: {
|
|
85
|
+
file: this.componentPath('runtime/DashboardPage.vue'),
|
|
86
|
+
meta: {
|
|
87
|
+
title: 'Dashboard',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
ensureDefaultDashboardConfig() {
|
|
94
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
const dashboardConfigs = this.adminforth.resource(this.options.dashboardConfigsResourceId);
|
|
96
|
+
const dashboardsCount = yield dashboardConfigs.count();
|
|
97
|
+
if (dashboardsCount > 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const createResult = yield dashboardConfigs.create({
|
|
101
|
+
id: randomUUID(),
|
|
102
|
+
slug: 'default',
|
|
103
|
+
label: 'Default Dashboard',
|
|
104
|
+
revision: 1,
|
|
105
|
+
config: DEFAULT_DASHBOARD_CONFIG,
|
|
106
|
+
});
|
|
107
|
+
if (!createResult.ok) {
|
|
108
|
+
throw new Error(createResult.error || 'Failed to create default dashboard config');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
setupEndpoints(server) {
|
|
113
|
+
const dashboardConfigService = createDashboardConfigService(this.adminforth, this.options.dashboardConfigsResourceId);
|
|
114
|
+
const widgetDataService = createWidgetDataService(this.adminforth);
|
|
115
|
+
const widgetConfigValidatorService = createWidgetConfigValidatorService(this.adminforth);
|
|
116
|
+
const ctx = Object.assign(Object.assign(Object.assign({ adminforth: this.adminforth, dashboardConfigsResourceId: this.options.dashboardConfigsResourceId, canEditDashboard }, dashboardConfigService), widgetDataService), widgetConfigValidatorService);
|
|
117
|
+
registerDashboardEndpoints(server, ctx);
|
|
118
|
+
registerGroupEndpoints(server, ctx);
|
|
119
|
+
registerWidgetEndpoints(server, ctx);
|
|
120
|
+
}
|
|
121
|
+
instanceUniqueRepresentation() {
|
|
122
|
+
return "dashboard";
|
|
123
|
+
}
|
|
124
|
+
}
|