kempo 4.5.1 → 4.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/admin/components/Dashboard.js +67 -0
- package/dist/admin/components/Dashboard.js.map +1 -0
- package/dist/admin/components/dashboardLayout.js +2 -0
- package/dist/admin/components/dashboardLayout.js.map +1 -0
- package/dist/admin/components/widgets/Content.js +4 -0
- package/dist/admin/components/widgets/Content.js.map +1 -0
- package/dist/admin/components/widgets/Extensions.js +5 -0
- package/dist/admin/components/widgets/Extensions.js.map +1 -0
- package/dist/admin/components/widgets/Notifications.js +4 -0
- package/dist/admin/components/widgets/Notifications.js.map +1 -0
- package/dist/admin/components/widgets/Pages.js +23 -0
- package/dist/admin/components/widgets/Pages.js.map +1 -0
- package/dist/admin/components/widgets/Users.js +5 -0
- package/dist/admin/components/widgets/Users.js.map +1 -0
- package/dist/admin/index.page.html +2 -1
- package/dist/admin/nav.fragment.html +57 -58
- package/dist/admin/realtime/index.page.html +2 -2
- package/dist/kempo/api/dashboard/widgets/GET.js +2 -0
- package/dist/kempo/api/dashboard/widgets/GET.js.map +1 -0
- package/dist/kempo/api/notifications/GET.js +1 -1
- package/dist/kempo/api/notifications/[id]/DELETE.js +1 -1
- package/dist/kempo/api/notifications/[id]/handled/POST.js +1 -1
- package/dist/kempo/api/notifications/[id]/read/POST.js +1 -1
- package/dist/kempo/api/notifications/count/GET.js +1 -1
- package/dist/kempo/api/notifications/read-all/POST.js +1 -1
- package/dist/kempo/components/AdminNavFooter.js +2 -0
- package/dist/kempo/components/AdminNavFooter.js.map +1 -0
- package/dist/kempo/components/DashboardWidget.js +18 -0
- package/dist/kempo/components/DashboardWidget.js.map +1 -0
- package/dist/kempo/components/Login.js +1 -1
- package/dist/kempo/components/NotificationBell.js +3 -3
- package/dist/kempo/components/NotificationBell.js.map +1 -1
- package/dist/kempo/components/NotificationList.js +1 -1
- package/dist/kempo/components/Register.js +1 -1
- package/dist/kempo/sdk.js +2 -2
- package/dist/kempo/sdk.js.map +1 -1
- package/dist/kempo/utils/safeNext.js +1 -1
- package/package.json +1 -1
- package/server/db/schema.js +34 -1
- package/server/sdk.js +8 -0
- package/server/utils/dashboard/coreWidgets.js +87 -0
- package/server/utils/dashboard/helpers.js +144 -0
- package/server/utils/dashboard/listDashboardWidgets.js +63 -0
- package/server/utils/dashboard/registerDashboardWidget.js +40 -0
- package/server/utils/dashboard/unregisterDashboardWidget.js +24 -0
- package/server/utils/extensions/installExtension.js +11 -0
- package/server/utils/extensions/uninstallExtension.js +2 -1
- package/server/utils/extensions/updateExtension.js +18 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import db from '../../db/index.js';
|
|
3
|
+
import { dashboardWidget, extension } from '../../db/schema.js';
|
|
4
|
+
import userHasPermission from '../permissions/userHasPermission.js';
|
|
5
|
+
import coreWidgets from './coreWidgets.js';
|
|
6
|
+
|
|
7
|
+
const publicFields = ({ createdAt, updatedAt, ...widget }) => widget;
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Every widget the dashboard can show: Kempo's own plus those extensions registered. Widgets of a
|
|
11
|
+
disabled extension are left out, since the module they point at is no longer served.
|
|
12
|
+
|
|
13
|
+
Pass `userId` to also drop widgets that person lacks the `permission` for; the check runs once per
|
|
14
|
+
distinct permission, and administrators hold every one. The same applies to each action's own
|
|
15
|
+
`permission`. Without `userId` nothing is filtered by permission.
|
|
16
|
+
|
|
17
|
+
`unavailable` lists the ids of widgets that exist but are not offered to this person, because their
|
|
18
|
+
extension is disabled or they lack the permission. A client that remembers a layout keeps those entries
|
|
19
|
+
so the widget returns to its place when it becomes available again, and drops entries for ids that are in
|
|
20
|
+
neither list, which is what an uninstalled extension looks like.
|
|
21
|
+
|
|
22
|
+
Ordered by `priority`, then title. If the widget table has not been created yet (a site that upgraded
|
|
23
|
+
without applying the schema), Kempo's own widgets are still returned so the dashboard is not blank.
|
|
24
|
+
*/
|
|
25
|
+
export default async ({ userId } = {}) => {
|
|
26
|
+
let registered = [];
|
|
27
|
+
const unavailable = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const rows = await db
|
|
31
|
+
.select({ widget: dashboardWidget, enabled: extension.enabled, installed: extension.name })
|
|
32
|
+
.from(dashboardWidget)
|
|
33
|
+
.leftJoin(extension, eq(extension.name, dashboardWidget.owner));
|
|
34
|
+
rows.forEach(({ widget, enabled, installed }) => {
|
|
35
|
+
if(installed && !enabled) unavailable.push(widget.id);
|
|
36
|
+
else registered.push(publicFields(widget));
|
|
37
|
+
});
|
|
38
|
+
} catch(error) {
|
|
39
|
+
console.warn(`Dashboard widgets from extensions are unavailable: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let widgets = [...coreWidgets, ...registered];
|
|
43
|
+
|
|
44
|
+
if(userId){
|
|
45
|
+
const permissions = [...new Set(widgets.flatMap(widget => [widget.permission, ...(widget.actions || []).map(action => action.permission)]).filter(Boolean))];
|
|
46
|
+
const granted = new Set();
|
|
47
|
+
|
|
48
|
+
await Promise.all(permissions.map(async permission => {
|
|
49
|
+
const [error, allowed] = await userHasPermission(userId, permission);
|
|
50
|
+
if(!error && allowed) granted.add(permission);
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
widgets.filter(widget => widget.permission && !granted.has(widget.permission)).forEach(widget => unavailable.push(widget.id));
|
|
54
|
+
|
|
55
|
+
widgets = widgets
|
|
56
|
+
.filter(widget => !widget.permission || granted.has(widget.permission))
|
|
57
|
+
.map(widget => ({ ...widget, actions: (widget.actions || []).filter(action => !action.permission || granted.has(action.permission)) }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
widgets.sort((a, b) => a.priority - b.priority || a.title.localeCompare(b.title));
|
|
61
|
+
|
|
62
|
+
return [null, { widgets, unavailable }];
|
|
63
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import db from '../../db/index.js';
|
|
2
|
+
import { dashboardWidget } from '../../db/schema.js';
|
|
3
|
+
import { CORE_OWNER, normalizeWidget } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Adds a widget to the admin dashboard, or updates it when `owner` and `key` already exist, so an
|
|
7
|
+
extension can call this from `install.js` and again from `update.js` without checking first.
|
|
8
|
+
|
|
9
|
+
`element` is the custom element the dashboard creates and `module` is the site path of the script
|
|
10
|
+
that defines it. The element loads its own data when it connects, so nothing here is fetched until a
|
|
11
|
+
user has the widget on their dashboard. `permission` hides it from people who do not hold it.
|
|
12
|
+
|
|
13
|
+
Sizes are in blocks: width 1 to 4, height 1 to 3. `minWidth`/`maxWidth`/`minHeight`/`maxHeight` bound
|
|
14
|
+
what a user may resize it to; `priority` orders it among the defaults, lowest first.
|
|
15
|
+
*/
|
|
16
|
+
export default async definition => {
|
|
17
|
+
const [invalid, widget] = normalizeWidget(definition);
|
|
18
|
+
|
|
19
|
+
if(invalid){
|
|
20
|
+
return [invalid, null];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if(widget.owner === CORE_OWNER){
|
|
24
|
+
return [{ code: 400, msg: `Owner "${CORE_OWNER}" is reserved for Kempo's own widgets` }, null];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const { id, ...changes } = widget;
|
|
30
|
+
const [saved] = await db
|
|
31
|
+
.insert(dashboardWidget)
|
|
32
|
+
.values({ ...widget, createdAt: now, updatedAt: now })
|
|
33
|
+
.onConflictDoUpdate({ target: dashboardWidget.id, set: { ...changes, updatedAt: now } })
|
|
34
|
+
.returning();
|
|
35
|
+
|
|
36
|
+
return [null, saved];
|
|
37
|
+
} catch(error) {
|
|
38
|
+
return [{ code: 500, msg: 'Failed to register dashboard widget' }, null];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm';
|
|
2
|
+
import db from '../../db/index.js';
|
|
3
|
+
import { dashboardWidget } from '../../db/schema.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Removes one widget, or every widget the owner registered when `key` is omitted. Removing something
|
|
7
|
+
that is not there is not an error; `removed` says how many rows went.
|
|
8
|
+
*/
|
|
9
|
+
export default async ({ owner, key } = {}) => {
|
|
10
|
+
if(!owner){
|
|
11
|
+
return [{ code: 400, msg: 'Owner is required' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const removed = await db
|
|
16
|
+
.delete(dashboardWidget)
|
|
17
|
+
.where(key ? and(eq(dashboardWidget.owner, owner), eq(dashboardWidget.key, key)) : eq(dashboardWidget.owner, owner))
|
|
18
|
+
.returning({ id: dashboardWidget.id });
|
|
19
|
+
|
|
20
|
+
return [null, { removed: removed.length }];
|
|
21
|
+
} catch(error) {
|
|
22
|
+
return [{ code: 500, msg: 'Failed to remove dashboard widget' }, null];
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -8,6 +8,7 @@ import { readFile } from 'fs/promises';
|
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
10
10
|
import createHook from '../hooks/createHook.js';
|
|
11
|
+
import registerDashboardWidget from '../dashboard/registerDashboardWidget.js';
|
|
11
12
|
import setSetting from '../settings/setSetting.js';
|
|
12
13
|
import declaredValue from '../settings/declaredValue.js';
|
|
13
14
|
import createTablesFromSchema from './createTablesFromSchema.js';
|
|
@@ -122,6 +123,16 @@ export default async ({ name }) => {
|
|
|
122
123
|
await createHook({ owner: name, event, callback });
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/*
|
|
127
|
+
Register declarative dashboard widgets
|
|
128
|
+
*/
|
|
129
|
+
for(const widget of kempoConfig.widgets || []){
|
|
130
|
+
const [widgetError] = await registerDashboardWidget({ ...widget, owner: name });
|
|
131
|
+
if(widgetError){
|
|
132
|
+
return [{ code: widgetError.code, msg: `Invalid dashboard widget "${widget.key}": ${widgetError.msg}` }, null];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
const now = new Date();
|
|
126
137
|
const entry = {
|
|
127
138
|
name,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
|
-
import { extension, hook, permission, setting, group, groupPermission, userGroup } from '../../db/schema.js';
|
|
2
|
+
import { extension, hook, permission, setting, group, groupPermission, userGroup, dashboardWidget } from '../../db/schema.js';
|
|
3
3
|
import { eq, inArray, like } from 'drizzle-orm';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
@@ -109,6 +109,7 @@ export default async ({ name, purgeData = false }) => {
|
|
|
109
109
|
|
|
110
110
|
try {
|
|
111
111
|
await db.delete(hook).where(eq(hook.owner, name));
|
|
112
|
+
await db.delete(dashboardWidget).where(eq(dashboardWidget.owner, name));
|
|
112
113
|
await db.delete(extension).where(eq(extension.name, name));
|
|
113
114
|
} catch(error) {
|
|
114
115
|
return [{ code: 500, msg: 'Failed to remove extension' }, null];
|
|
@@ -8,6 +8,8 @@ import { randomUUID } from 'crypto';
|
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
10
10
|
import createHook from '../hooks/createHook.js';
|
|
11
|
+
import registerDashboardWidget from '../dashboard/registerDashboardWidget.js';
|
|
12
|
+
import unregisterDashboardWidget from '../dashboard/unregisterDashboardWidget.js';
|
|
11
13
|
import setSetting from '../settings/setSetting.js';
|
|
12
14
|
import declaredValue from '../settings/declaredValue.js';
|
|
13
15
|
import createTablesFromSchema from './createTablesFromSchema.js';
|
|
@@ -141,6 +143,22 @@ export default async ({ name, version = 'latest' }) => {
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/*
|
|
147
|
+
Sync dashboard widgets: register or refresh every declared one, remove those no longer declared
|
|
148
|
+
*/
|
|
149
|
+
const newWidgetKeys = new Set((newKempo.widgets || []).map(w => w.key));
|
|
150
|
+
for(const widget of newKempo.widgets || []){
|
|
151
|
+
const [widgetError] = await registerDashboardWidget({ ...widget, owner: name });
|
|
152
|
+
if(widgetError){
|
|
153
|
+
return [{ code: widgetError.code, msg: `Invalid dashboard widget "${widget.key}": ${widgetError.msg}` }, null];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for(const { key } of oldKempo.widgets || []){
|
|
157
|
+
if(!newWidgetKeys.has(key)){
|
|
158
|
+
await unregisterDashboardWidget({ owner: name, key });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
const now = new Date();
|
|
145
163
|
try {
|
|
146
164
|
await db.update(extension).set({
|