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.
Files changed (49) hide show
  1. package/README.md +5 -0
  2. package/dist/admin/components/Dashboard.js +67 -0
  3. package/dist/admin/components/Dashboard.js.map +1 -0
  4. package/dist/admin/components/dashboardLayout.js +2 -0
  5. package/dist/admin/components/dashboardLayout.js.map +1 -0
  6. package/dist/admin/components/widgets/Content.js +4 -0
  7. package/dist/admin/components/widgets/Content.js.map +1 -0
  8. package/dist/admin/components/widgets/Extensions.js +5 -0
  9. package/dist/admin/components/widgets/Extensions.js.map +1 -0
  10. package/dist/admin/components/widgets/Notifications.js +4 -0
  11. package/dist/admin/components/widgets/Notifications.js.map +1 -0
  12. package/dist/admin/components/widgets/Pages.js +23 -0
  13. package/dist/admin/components/widgets/Pages.js.map +1 -0
  14. package/dist/admin/components/widgets/Users.js +5 -0
  15. package/dist/admin/components/widgets/Users.js.map +1 -0
  16. package/dist/admin/index.page.html +2 -1
  17. package/dist/admin/nav.fragment.html +57 -58
  18. package/dist/admin/realtime/index.page.html +2 -2
  19. package/dist/kempo/api/dashboard/widgets/GET.js +2 -0
  20. package/dist/kempo/api/dashboard/widgets/GET.js.map +1 -0
  21. package/dist/kempo/api/notifications/GET.js +1 -1
  22. package/dist/kempo/api/notifications/[id]/DELETE.js +1 -1
  23. package/dist/kempo/api/notifications/[id]/handled/POST.js +1 -1
  24. package/dist/kempo/api/notifications/[id]/read/POST.js +1 -1
  25. package/dist/kempo/api/notifications/count/GET.js +1 -1
  26. package/dist/kempo/api/notifications/read-all/POST.js +1 -1
  27. package/dist/kempo/components/AdminNavFooter.js +2 -0
  28. package/dist/kempo/components/AdminNavFooter.js.map +1 -0
  29. package/dist/kempo/components/DashboardWidget.js +18 -0
  30. package/dist/kempo/components/DashboardWidget.js.map +1 -0
  31. package/dist/kempo/components/Login.js +1 -1
  32. package/dist/kempo/components/NotificationBell.js +3 -3
  33. package/dist/kempo/components/NotificationBell.js.map +1 -1
  34. package/dist/kempo/components/NotificationList.js +1 -1
  35. package/dist/kempo/components/Register.js +1 -1
  36. package/dist/kempo/sdk.js +2 -2
  37. package/dist/kempo/sdk.js.map +1 -1
  38. package/dist/kempo/utils/safeNext.js +1 -1
  39. package/package.json +1 -1
  40. package/server/db/schema.js +34 -1
  41. package/server/sdk.js +8 -0
  42. package/server/utils/dashboard/coreWidgets.js +87 -0
  43. package/server/utils/dashboard/helpers.js +144 -0
  44. package/server/utils/dashboard/listDashboardWidgets.js +63 -0
  45. package/server/utils/dashboard/registerDashboardWidget.js +40 -0
  46. package/server/utils/dashboard/unregisterDashboardWidget.js +24 -0
  47. package/server/utils/extensions/installExtension.js +11 -0
  48. package/server/utils/extensions/uninstallExtension.js +2 -1
  49. 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({