@rsc-kit/press 0.0.0-stage → 0.30.43
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 +117 -2
- package/dist/admin/actions.d.ts +30 -0
- package/dist/admin/actions.js +171 -0
- package/dist/admin/actions.js.map +1 -0
- package/dist/admin/index.d.ts +5 -0
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/panel.d.ts +96 -0
- package/dist/admin/panel.js +354 -0
- package/dist/admin/panel.js.map +1 -0
- package/dist/admin/registry.d.ts +58 -0
- package/dist/admin/registry.js +90 -0
- package/dist/admin/registry.js.map +1 -0
- package/dist/admin/theme.d.ts +251 -0
- package/dist/admin/theme.js +2 -0
- package/dist/admin/theme.js.map +1 -0
- package/dist/admin/url.d.ts +24 -0
- package/dist/admin/url.js +45 -0
- package/dist/admin/url.js.map +1 -0
- package/dist/blocks.d.ts +46 -0
- package/dist/blocks.js +33 -0
- package/dist/blocks.js.map +1 -0
- package/dist/forms/actions.d.ts +7 -0
- package/dist/forms/actions.js +26 -0
- package/dist/forms/actions.js.map +1 -0
- package/dist/forms/form.d.ts +42 -0
- package/dist/forms/form.js +44 -0
- package/dist/forms/form.js.map +1 -0
- package/dist/forms/honeypot.d.ts +2 -0
- package/dist/forms/honeypot.js +3 -0
- package/dist/forms/honeypot.js.map +1 -0
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +10 -0
- package/dist/forms/registry.js +17 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/theme.d.ts +29 -0
- package/dist/forms/theme.js +2 -0
- package/dist/forms/theme.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/installable.d.ts +173 -0
- package/dist/installable.js +490 -0
- package/dist/installable.js.map +1 -0
- package/dist/memory.d.ts +7 -0
- package/dist/memory.js +50 -0
- package/dist/memory.js.map +1 -0
- package/dist/pages/index.d.ts +2 -0
- package/dist/pages/index.js +2 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/pages.d.ts +78 -0
- package/dist/pages/pages.js +183 -0
- package/dist/pages/pages.js.map +1 -0
- package/dist/plugin.d.ts +51 -0
- package/dist/plugin.js +20 -0
- package/dist/plugin.js.map +1 -0
- package/dist/resource.d.ts +320 -0
- package/dist/resource.js +415 -0
- package/dist/resource.js.map +1 -0
- package/dist/shadcn/breadcrumbs.d.ts +6 -0
- package/dist/shadcn/breadcrumbs.js +21 -0
- package/dist/shadcn/breadcrumbs.js.map +1 -0
- package/dist/shadcn/components/ui/alert-dialog.d.ts +18 -0
- package/dist/shadcn/components/ui/alert-dialog.js +44 -0
- package/dist/shadcn/components/ui/alert-dialog.js.map +1 -0
- package/dist/shadcn/components/ui/avatar.d.ts +11 -0
- package/dist/shadcn/components/ui/avatar.js +25 -0
- package/dist/shadcn/components/ui/avatar.js.map +1 -0
- package/dist/shadcn/components/ui/badge.d.ts +7 -0
- package/dist/shadcn/components/ui/badge.js +34 -0
- package/dist/shadcn/components/ui/badge.js.map +1 -0
- package/dist/shadcn/components/ui/breadcrumb.d.ts +10 -0
- package/dist/shadcn/components/ui/breadcrumb.js +38 -0
- package/dist/shadcn/components/ui/breadcrumb.js.map +1 -0
- package/dist/shadcn/components/ui/button.d.ts +8 -0
- package/dist/shadcn/components/ui/button.js +35 -0
- package/dist/shadcn/components/ui/button.js.map +1 -0
- package/dist/shadcn/components/ui/card.d.ts +11 -0
- package/dist/shadcn/components/ui/card.js +26 -0
- package/dist/shadcn/components/ui/card.js.map +1 -0
- package/dist/shadcn/components/ui/checkbox.d.ts +3 -0
- package/dist/shadcn/components/ui/checkbox.js +10 -0
- package/dist/shadcn/components/ui/checkbox.js.map +1 -0
- package/dist/shadcn/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/shadcn/components/ui/dropdown-menu.js +53 -0
- package/dist/shadcn/components/ui/dropdown-menu.js.map +1 -0
- package/dist/shadcn/components/ui/field.d.ts +24 -0
- package/dist/shadcn/components/ui/field.js +69 -0
- package/dist/shadcn/components/ui/field.js.map +1 -0
- package/dist/shadcn/components/ui/input.d.ts +3 -0
- package/dist/shadcn/components/ui/input.js +9 -0
- package/dist/shadcn/components/ui/input.js.map +1 -0
- package/dist/shadcn/components/ui/label.d.ts +3 -0
- package/dist/shadcn/components/ui/label.js +9 -0
- package/dist/shadcn/components/ui/label.js.map +1 -0
- package/dist/shadcn/components/ui/select.d.ts +15 -0
- package/dist/shadcn/components/ui/select.js +36 -0
- package/dist/shadcn/components/ui/select.js.map +1 -0
- package/dist/shadcn/components/ui/separator.d.ts +3 -0
- package/dist/shadcn/components/ui/separator.js +9 -0
- package/dist/shadcn/components/ui/separator.js.map +1 -0
- package/dist/shadcn/components/ui/sheet.d.ts +14 -0
- package/dist/shadcn/components/ui/sheet.js +39 -0
- package/dist/shadcn/components/ui/sheet.js.map +1 -0
- package/dist/shadcn/components/ui/sidebar.d.ts +63 -0
- package/dist/shadcn/components/ui/sidebar.js +257 -0
- package/dist/shadcn/components/ui/sidebar.js.map +1 -0
- package/dist/shadcn/components/ui/skeleton.d.ts +2 -0
- package/dist/shadcn/components/ui/skeleton.js +7 -0
- package/dist/shadcn/components/ui/skeleton.js.map +1 -0
- package/dist/shadcn/components/ui/sonner.d.ts +3 -0
- package/dist/shadcn/components/ui/sonner.js +38 -0
- package/dist/shadcn/components/ui/sonner.js.map +1 -0
- package/dist/shadcn/components/ui/switch.d.ts +5 -0
- package/dist/shadcn/components/ui/switch.js +9 -0
- package/dist/shadcn/components/ui/switch.js.map +1 -0
- package/dist/shadcn/components/ui/table.d.ts +10 -0
- package/dist/shadcn/components/ui/table.js +30 -0
- package/dist/shadcn/components/ui/table.js.map +1 -0
- package/dist/shadcn/components/ui/textarea.d.ts +3 -0
- package/dist/shadcn/components/ui/textarea.js +8 -0
- package/dist/shadcn/components/ui/textarea.js.map +1 -0
- package/dist/shadcn/components/ui/tooltip.d.ts +6 -0
- package/dist/shadcn/components/ui/tooltip.js +18 -0
- package/dist/shadcn/components/ui/tooltip.js.map +1 -0
- package/dist/shadcn/data-table.d.ts +12 -0
- package/dist/shadcn/data-table.js +195 -0
- package/dist/shadcn/data-table.js.map +1 -0
- package/dist/shadcn/fields.d.ts +20 -0
- package/dist/shadcn/fields.js +179 -0
- package/dist/shadcn/fields.js.map +1 -0
- package/dist/shadcn/flash-toasts.d.ts +2 -0
- package/dist/shadcn/flash-toasts.js +11 -0
- package/dist/shadcn/flash-toasts.js.map +1 -0
- package/dist/shadcn/hooks/use-mobile.d.ts +1 -0
- package/dist/shadcn/hooks/use-mobile.js +16 -0
- package/dist/shadcn/hooks/use-mobile.js.map +1 -0
- package/dist/shadcn/index.d.ts +10 -0
- package/dist/shadcn/index.js +31 -0
- package/dist/shadcn/index.js.map +1 -0
- package/dist/shadcn/mode-toggle.d.ts +7 -0
- package/dist/shadcn/mode-toggle.js +52 -0
- package/dist/shadcn/mode-toggle.js.map +1 -0
- package/dist/shadcn/nav.d.ts +5 -0
- package/dist/shadcn/nav.js +19 -0
- package/dist/shadcn/nav.js.map +1 -0
- package/dist/shadcn/pages.d.ts +15 -0
- package/dist/shadcn/pages.js +59 -0
- package/dist/shadcn/pages.js.map +1 -0
- package/dist/shadcn/plugin-forms.d.ts +16 -0
- package/dist/shadcn/plugin-forms.js +25 -0
- package/dist/shadcn/plugin-forms.js.map +1 -0
- package/dist/shadcn/public-form.d.ts +8 -0
- package/dist/shadcn/public-form.js +26 -0
- package/dist/shadcn/public-form.js.map +1 -0
- package/dist/shadcn/resource-form.d.ts +19 -0
- package/dist/shadcn/resource-form.js +30 -0
- package/dist/shadcn/resource-form.js.map +1 -0
- package/dist/shadcn/styles.css +12 -0
- package/dist/shadcn/tenant-switcher.d.ts +3 -0
- package/dist/shadcn/tenant-switcher.js +17 -0
- package/dist/shadcn/tenant-switcher.js.map +1 -0
- package/dist/shadcn/theme-script.d.ts +7 -0
- package/dist/shadcn/theme-script.js +10 -0
- package/dist/shadcn/theme-script.js.map +1 -0
- package/dist/shadcn/user-menu.d.ts +3 -0
- package/dist/shadcn/user-menu.js +23 -0
- package/dist/shadcn/user-menu.js.map +1 -0
- package/dist/signing.d.ts +43 -0
- package/dist/signing.js +82 -0
- package/dist/signing.js.map +1 -0
- package/dist/tenant.d.ts +12 -0
- package/dist/tenant.js +29 -0
- package/dist/tenant.js.map +1 -0
- package/dist/uploads.d.ts +32 -0
- package/dist/uploads.js +28 -0
- package/dist/uploads.js.map +1 -0
- package/package.json +74 -4
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { notFound } from '@rsc-kit/core/not-found';
|
|
4
|
+
import { connection } from '@rsc-kit/core/request';
|
|
5
|
+
import { section } from '@rsc-kit/core/section';
|
|
6
|
+
import { bulkRecords, deleteRecord, installPlugin, reviewPlugin, saveRecord, secretCopied, signOut, switchTenant, uninstallPlugin, } from './actions.js';
|
|
7
|
+
import { digestOf, fetchManifest, RESERVED, scopeWords } from '../installable.js';
|
|
8
|
+
import { allowOf, contextOf, registerPanel, resourcesFor } from './registry.js';
|
|
9
|
+
import { humanize } from '../resource.js';
|
|
10
|
+
export { listSearch, pluginsSearch, recordParams, resourceParams } from './url.js';
|
|
11
|
+
/**
|
|
12
|
+
* A panel: its resources, and the pages that show them, already bound to it.
|
|
13
|
+
* The app mounts each in a route file of its own, because the engine finds
|
|
14
|
+
* routes, url schemas and sections by their files:
|
|
15
|
+
*
|
|
16
|
+
* src/app/admin/middleware.ts export default panel.middleware
|
|
17
|
+
* src/app/admin/layout.tsx export default panel.Layout
|
|
18
|
+
* src/app/admin/page.tsx export default panel.Dashboard
|
|
19
|
+
* src/app/admin/[resource]/page.tsx export const params = resourceParams
|
|
20
|
+
* export const searchParams = listSearch
|
|
21
|
+
* export default panel.Index
|
|
22
|
+
* src/app/admin/[resource]/admin-table.section.tsx export default panel.Table
|
|
23
|
+
* src/app/admin/[resource]/new/page.tsx export const params = resourceParams
|
|
24
|
+
* export default panel.Create
|
|
25
|
+
* src/app/admin/[resource]/[id]/page.tsx export const params = recordParams
|
|
26
|
+
* export default panel.Edit
|
|
27
|
+
*/
|
|
28
|
+
export function definePanel(definition) {
|
|
29
|
+
const path = definition.path ?? '/admin';
|
|
30
|
+
const title = definition.title ?? 'Admin';
|
|
31
|
+
const plugins = definition.plugins ?? [];
|
|
32
|
+
const resources = [...definition.resources, ...plugins.flatMap((plugin) => plugin.resources ?? [])];
|
|
33
|
+
const authorize = definition.authorize ?? (() => true);
|
|
34
|
+
const tenancy = definition.tenancy ?? null;
|
|
35
|
+
const installs = definition.installs ?? null;
|
|
36
|
+
const theme = definition.theme;
|
|
37
|
+
/** The panel's own resources and its plugins'; any other is one a tenant installed. */
|
|
38
|
+
const own = new Set(resources.map((resource) => resource.name));
|
|
39
|
+
if (!/^(\/[a-z0-9-]+)+$/.test(path)) {
|
|
40
|
+
throw new Error(`[admin] a panel's path is where its routes are, with no trailing slash: ${JSON.stringify(path)}`);
|
|
41
|
+
}
|
|
42
|
+
const names = resources.map((resource) => resource.name);
|
|
43
|
+
const twice = names.find((name, index) => names.indexOf(name) !== index);
|
|
44
|
+
if (twice)
|
|
45
|
+
throw new Error(`[admin] two resources are named ${twice}; a name is a url segment`);
|
|
46
|
+
const reserved = installs ? names.find((name) => RESERVED.includes(name)) : undefined;
|
|
47
|
+
if (reserved)
|
|
48
|
+
throw new Error(`[admin] a panel that takes installs has a plugins page, so no resource may be named ${reserved}`);
|
|
49
|
+
const pluginNames = plugins.map((plugin) => plugin.name);
|
|
50
|
+
const pluginTwice = pluginNames.find((name, index) => pluginNames.indexOf(name) !== index);
|
|
51
|
+
if (pluginTwice)
|
|
52
|
+
throw new Error(`[admin] the plugin ${pluginTwice} is installed twice`);
|
|
53
|
+
// A relation names a resource by its url segment. Checked here, once every
|
|
54
|
+
// resource is known, so a misspelt one fails at startup and not on the
|
|
55
|
+
// first form that needs its choices.
|
|
56
|
+
for (const resource of resources) {
|
|
57
|
+
const named = [...resource.fields.map((field) => field.relation), ...resource.columns.map((column) => column.relation)];
|
|
58
|
+
for (const relation of named) {
|
|
59
|
+
if (relation !== undefined && !names.includes(relation)) {
|
|
60
|
+
throw new Error(`[admin] ${resource.name} relates to ${relation}, which is not a resource of this panel`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const handle = { path, resources, authorize, plugins, tenancy, signOut: definition.signOut ?? null, installs };
|
|
65
|
+
registerPanel(handle);
|
|
66
|
+
/** A url of the panel. Typed routes are the app's; the panel's are its own. */
|
|
67
|
+
const to = (...parts) => [path, ...parts].join('/');
|
|
68
|
+
/**
|
|
69
|
+
* A related resource's rows as choices: its key saved, its title shown.
|
|
70
|
+
* Every row, scoped to the visitor's tenant like any list - so a belongsTo
|
|
71
|
+
* is for a resource with a select's worth of rows: categories, teams,
|
|
72
|
+
* plans. Memoised per render, so a field and a column naming the same
|
|
73
|
+
* resource list it once.
|
|
74
|
+
*/
|
|
75
|
+
function choices(context, resources) {
|
|
76
|
+
const listed = new Map();
|
|
77
|
+
return (name) => {
|
|
78
|
+
let found = listed.get(name);
|
|
79
|
+
if (!found) {
|
|
80
|
+
const related = resources.find((resource) => resource.name === name);
|
|
81
|
+
found = related.source
|
|
82
|
+
.list({ dir: 'asc', page: 1, perPage: null, filters: {}, searchable: [] }, context)
|
|
83
|
+
.then(({ rows }) => rows.map((row) => ({ value: String(row[related.key]), label: String(row[related.title]) })));
|
|
84
|
+
listed.set(name, found);
|
|
85
|
+
}
|
|
86
|
+
return found;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** The form's fields, each relation given its choices. */
|
|
90
|
+
async function fieldsFor(resource, context) {
|
|
91
|
+
const of = choices(context, await resourcesFor(handle, context));
|
|
92
|
+
return Promise.all(resource.fields.map(async (field) => (field.relation ? { ...field, options: await of(field.relation) } : field)));
|
|
93
|
+
}
|
|
94
|
+
async function enter(name) {
|
|
95
|
+
await connection();
|
|
96
|
+
if (!(await authorize()))
|
|
97
|
+
notFound();
|
|
98
|
+
const context = (await contextOf(handle)) ?? notFound();
|
|
99
|
+
if (name === undefined)
|
|
100
|
+
return context;
|
|
101
|
+
return { resource: (await resourcesFor(handle, context)).find((resource) => resource.name === name) ?? notFound(), context };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The panel's guard, mounted as src/app/admin/middleware.ts. It runs before
|
|
105
|
+
* anything is sent, so its refusals are real 404s: a visitor authorize turns
|
|
106
|
+
* away, no tenant or another's, a resource the panel does not have, a record
|
|
107
|
+
* that is not there - or not this tenant's. The pages check again, inside
|
|
108
|
+
* their boundaries, where a refusal can no longer change the status.
|
|
109
|
+
*/
|
|
110
|
+
async function middleware(params = {}) {
|
|
111
|
+
if (!(await authorize()))
|
|
112
|
+
notFound();
|
|
113
|
+
const context = (await contextOf(handle)) ?? notFound();
|
|
114
|
+
if (params.resource === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const resource = (await resourcesFor(handle, context)).find((candidate) => candidate.name === params.resource) ?? notFound();
|
|
117
|
+
// An edit page changes a record. A read-only resource's new page cannot
|
|
118
|
+
// be told apart here - it has the list's params - so it refuses itself.
|
|
119
|
+
if (resource.readOnly && params.id !== undefined)
|
|
120
|
+
notFound();
|
|
121
|
+
if (params.id !== undefined && !(await resource.source.find(params.id, context)))
|
|
122
|
+
notFound();
|
|
123
|
+
}
|
|
124
|
+
/** The sidebar's header: the tenant switcher, or the panel's name. */
|
|
125
|
+
async function Tenants() {
|
|
126
|
+
await connection();
|
|
127
|
+
const context = (await authorize()) && tenancy ? await contextOf(handle) : null;
|
|
128
|
+
if (!tenancy || !context?.tenant)
|
|
129
|
+
return brand();
|
|
130
|
+
return (_jsx(theme.TenantSwitcher, { tenants: await tenancy.tenants(), current: context.tenant, label: tenancy.label ?? 'Teams', choose: switchTenant.bind(null, path) }));
|
|
131
|
+
}
|
|
132
|
+
function brand() {
|
|
133
|
+
const Logo = definition.logo;
|
|
134
|
+
return _jsx(theme.Brand, { href: path, title: title, ...(Logo ? { logo: _jsx(Logo, {}) } : {}) });
|
|
135
|
+
}
|
|
136
|
+
async function User() {
|
|
137
|
+
await connection();
|
|
138
|
+
const user = (await authorize()) ? await definition.user?.() : null;
|
|
139
|
+
if (!user)
|
|
140
|
+
return null;
|
|
141
|
+
return _jsx(theme.UserMenu, { user: user, ...(definition.signOut ? { signOut: signOut.bind(null, path) } : {}) });
|
|
142
|
+
}
|
|
143
|
+
/** A resource's own icon, drawn; the theme draws a default for one without. */
|
|
144
|
+
function iconOf(resource) {
|
|
145
|
+
const Icon = resource.icon;
|
|
146
|
+
return Icon ? { icon: _jsx(Icon, {}) } : {};
|
|
147
|
+
}
|
|
148
|
+
/** The sidebar's links: the panel's resources, then the tenant's installed ones, then Plugins. */
|
|
149
|
+
function navOf(shown) {
|
|
150
|
+
return [
|
|
151
|
+
{ href: path, label: 'Dashboard', kind: 'dashboard', exact: true },
|
|
152
|
+
...shown.map((resource) => ({
|
|
153
|
+
href: to(resource.name),
|
|
154
|
+
label: resource.label,
|
|
155
|
+
kind: own.has(resource.name) ? 'resource' : 'installed',
|
|
156
|
+
...iconOf(resource),
|
|
157
|
+
})),
|
|
158
|
+
...(installs ? [{ href: to('plugins'), label: 'Plugins', kind: 'plugins' }] : []),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
/** Installed resources are this tenant's and this request's, so the nav waits for them; the panel's own show meanwhile. */
|
|
162
|
+
async function InstalledNav() {
|
|
163
|
+
await connection();
|
|
164
|
+
const context = (await authorize()) ? await contextOf(handle) : null;
|
|
165
|
+
return _jsx(theme.Nav, { items: navOf(context ? await resourcesFor(handle, context) : resources) });
|
|
166
|
+
}
|
|
167
|
+
function crumbsOf(shown) {
|
|
168
|
+
return (_jsx(theme.Breadcrumbs, { root: path, title: title, labels: { ...(installs ? { plugins: 'Plugins' } : {}), ...Object.fromEntries(shown.map((resource) => [resource.name, resource.label])) } }));
|
|
169
|
+
}
|
|
170
|
+
async function InstalledCrumbs() {
|
|
171
|
+
await connection();
|
|
172
|
+
const context = (await authorize()) ? await contextOf(handle) : null;
|
|
173
|
+
return crumbsOf(context ? await resourcesFor(handle, context) : resources);
|
|
174
|
+
}
|
|
175
|
+
function Layout({ children }) {
|
|
176
|
+
return (_jsx(theme.Shell, { title: title, modeToggle: definition.modeToggle !== false, brand: tenancy ? (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "menu" }), children: _jsx(Tenants, {}) })) : (brand()), nav: installs ? (_jsx(Suspense, { fallback: _jsx(theme.Nav, { items: navOf(resources) }), children: _jsx(InstalledNav, {}) })) : (_jsx(theme.Nav, { items: navOf(resources) })), user: definition.user ? (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "menu" }), children: _jsx(User, {}) })) : null, crumbs: installs ? (_jsx(Suspense, { fallback: crumbsOf(resources), children: _jsx(InstalledCrumbs, {}) })) : (crumbsOf(resources)), children: children }));
|
|
177
|
+
}
|
|
178
|
+
async function DashboardBody() {
|
|
179
|
+
const context = await enter();
|
|
180
|
+
const counts = await Promise.all((await resourcesFor(handle, context)).map(async (resource) => {
|
|
181
|
+
// An installed plugin is somebody else's server: one that is down
|
|
182
|
+
// leaves its card blank, not the dashboard broken.
|
|
183
|
+
const total = await resource.source
|
|
184
|
+
.list({ dir: 'asc', page: 1, perPage: 1, filters: {}, searchable: [] }, context)
|
|
185
|
+
.then(({ total: counted }) => counted)
|
|
186
|
+
.catch((error) => {
|
|
187
|
+
console.error(`[admin] counting ${resource.name} failed`, error);
|
|
188
|
+
return null;
|
|
189
|
+
});
|
|
190
|
+
return { name: resource.name, label: resource.label, href: to(resource.name), total, ...iconOf(resource) };
|
|
191
|
+
}));
|
|
192
|
+
return (_jsx(theme.Dashboard, { description: context.tenant ? `Everything in ${context.tenant.name}.` : 'Everything in the panel, at a glance.', counts: counts, widgets: plugins.flatMap((plugin) => (plugin.widgets ?? []).map(({ title: widgetTitle, description, component: Widget }, index) => ({
|
|
193
|
+
key: `${plugin.name}:${index}`,
|
|
194
|
+
title: widgetTitle,
|
|
195
|
+
...(description ? { description } : {}),
|
|
196
|
+
content: _jsx(Widget, { tenant: context.tenant }),
|
|
197
|
+
}))) }));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The table, as a section: a delete refreshes it and nothing else. A
|
|
201
|
+
* refresh renders it with the page's params and searchParams, and nothing
|
|
202
|
+
* else - which is why it is bound to the panel here rather than handed one
|
|
203
|
+
* as a prop.
|
|
204
|
+
*/
|
|
205
|
+
async function TableBody({ params, searchParams }) {
|
|
206
|
+
const { resource, context } = await enter((await params).resource);
|
|
207
|
+
const search = await searchParams;
|
|
208
|
+
const { q, sort, dir, page, ...rest } = search;
|
|
209
|
+
const filters = Object.fromEntries(Object.entries(rest).flatMap(([key, value]) => typeof value === 'string' && value !== '' && resource.filters.some((filter) => filter.key === key) ? [[key, value]] : []));
|
|
210
|
+
const sortable = resource.columns.some((column) => column.sortable && column.key === sort) ? sort : undefined;
|
|
211
|
+
const server = resource.mode === 'server';
|
|
212
|
+
const searchable = resource.columns.filter((column) => column.searchable).map((column) => column.key);
|
|
213
|
+
const { rows, total } = await resource.source.list(server
|
|
214
|
+
? { ...(q ? { q } : {}), ...(sortable ? { sort: sortable } : {}), dir, page, perPage: resource.perPage, filters, searchable }
|
|
215
|
+
: { dir: 'asc', page: 1, perPage: null, filters: {}, searchable }, context);
|
|
216
|
+
// A relation column shows the related row's title, and is searched and
|
|
217
|
+
// sorted by it in the browser too: what the visitor reads is what they type.
|
|
218
|
+
const of = choices(context, await resourcesFor(handle, context));
|
|
219
|
+
const titles = new Map(await Promise.all(resource.columns
|
|
220
|
+
.filter((column) => column.relation !== undefined)
|
|
221
|
+
.map(async (column) => [column.key, new Map((await of(column.relation)).map((option) => [option.value, option.label]))])));
|
|
222
|
+
const shown = (column, value) => {
|
|
223
|
+
const title = value === null || value === undefined ? undefined : titles.get(column.key)?.get(String(value));
|
|
224
|
+
return title ?? value;
|
|
225
|
+
};
|
|
226
|
+
const drawn = await Promise.all(rows.map(async (row) => {
|
|
227
|
+
const id = String(row[resource.key]);
|
|
228
|
+
return {
|
|
229
|
+
id,
|
|
230
|
+
cells: Object.fromEntries(await Promise.all(resource.columns.map(async (column) => [column.key, await draw(theme, column, shown(column, row[column.key]), row)]))),
|
|
231
|
+
values: Object.fromEntries(resource.columns.map((column) => [column.key, plain(shown(column, row[column.key]))])),
|
|
232
|
+
...(resource.readOnly
|
|
233
|
+
? {}
|
|
234
|
+
: { edit: to(resource.name, encodeURIComponent(id)), remove: deleteRecord.bind(null, path, resource.name, id) }),
|
|
235
|
+
};
|
|
236
|
+
}));
|
|
237
|
+
return (_jsx(theme.Table, { mode: resource.mode, href: to(resource.name), columns: resource.columns.map(({ key, label, sortable: canSort, searchable: canSearch, visible }) => ({
|
|
238
|
+
key,
|
|
239
|
+
label,
|
|
240
|
+
sortable: canSort,
|
|
241
|
+
searchable: canSearch,
|
|
242
|
+
visible,
|
|
243
|
+
})), rows: drawn, total: total, perPage: resource.perPage, query: { q: q ?? '', ...(sortable ? { sort: sortable } : {}), dir, page, filters }, filters: resource.filters, noun: { one: resource.singular, many: resource.label }, ...(resource.readOnly
|
|
244
|
+
? {}
|
|
245
|
+
: {
|
|
246
|
+
bulk: {
|
|
247
|
+
actions: [
|
|
248
|
+
...resource.bulkActions.map(({ name, label, destructive }) => ({ name, label, destructive: destructive ?? false })),
|
|
249
|
+
{ name: 'delete', label: 'Delete', destructive: true },
|
|
250
|
+
],
|
|
251
|
+
run: bulkRecords.bind(null, path, resource.name),
|
|
252
|
+
},
|
|
253
|
+
}) }));
|
|
254
|
+
}
|
|
255
|
+
const Table = section('admin-table', TableBody);
|
|
256
|
+
async function IndexBody({ params, searchParams }) {
|
|
257
|
+
const { resource } = await enter((await params).resource);
|
|
258
|
+
return (_jsx(theme.Index, { title: resource.label, ...(resource.description ? { description: resource.description } : {}), ...(resource.readOnly ? {} : { create: { href: to(resource.name, 'new'), label: `New ${resource.singular.toLowerCase()}` } }), table: _jsx(Table, { params: params, searchParams: searchParams }) }));
|
|
259
|
+
}
|
|
260
|
+
async function CreateBody({ params }) {
|
|
261
|
+
const { resource, context } = await enter((await params).resource);
|
|
262
|
+
if (resource.readOnly)
|
|
263
|
+
notFound();
|
|
264
|
+
return (_jsx(theme.Form, { title: `New ${resource.singular.toLowerCase()}`, description: `Add a ${resource.singular.toLowerCase()} to ${resource.label.toLowerCase()}.`, action: saveRecord.bind(null, path, resource.name, null), fields: await fieldsFor(resource, context), values: {}, submit: `Create ${resource.singular.toLowerCase()}`, cancel: to(resource.name) }));
|
|
265
|
+
}
|
|
266
|
+
async function EditBody({ params }) {
|
|
267
|
+
const { resource: name, id } = await params;
|
|
268
|
+
const { resource, context } = await enter(name);
|
|
269
|
+
const record = resource.readOnly ? null : await resource.source.find(id, context);
|
|
270
|
+
if (!record)
|
|
271
|
+
notFound();
|
|
272
|
+
return (_jsx(theme.Form, { title: String(record[resource.title]), description: `Edit this ${resource.singular.toLowerCase()}.`, action: saveRecord.bind(null, path, resource.name, id), fields: await fieldsFor(resource, context), values: record, submit: "Save changes", cancel: to(resource.name) }));
|
|
273
|
+
}
|
|
274
|
+
/** The installed plugins, a manifest under review, and the form to start one. */
|
|
275
|
+
async function PluginsBody({ searchParams }) {
|
|
276
|
+
const context = await enter();
|
|
277
|
+
if (!installs)
|
|
278
|
+
notFound();
|
|
279
|
+
const allowed = !installs.manage || (await installs.manage());
|
|
280
|
+
const { review } = await searchParams;
|
|
281
|
+
const installed = await installs.store.list(context.tenant);
|
|
282
|
+
const labelOf = (name) => resources.find((resource) => resource.name === name)?.label ?? name;
|
|
283
|
+
let reviewing = null;
|
|
284
|
+
// Once installed, the review is over: the install re-renders this url in place, ?review= and all.
|
|
285
|
+
if (review && allowed && !installed.some((installation) => installation.manifestUrl === review)) {
|
|
286
|
+
try {
|
|
287
|
+
const manifest = await fetchManifest(review, allowOf(handle), resources.map((resource) => resource.name));
|
|
288
|
+
reviewing = {
|
|
289
|
+
label: manifest.label,
|
|
290
|
+
...(manifest.version ? { version: manifest.version } : {}),
|
|
291
|
+
description: manifest.description ?? new URL(manifest.endpoint).host,
|
|
292
|
+
adds: manifest.resources.map((resource) => resource.label ?? humanize(resource.name)),
|
|
293
|
+
can: manifest.scopes.length === 0 ? [] : scopeWords(manifest, labelOf),
|
|
294
|
+
endpoint: manifest.endpoint,
|
|
295
|
+
install: installPlugin.bind(null, path, review, await digestOf(manifest)),
|
|
296
|
+
cancel: to('plugins'),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
reviewing = { problem: error instanceof Error ? error.message : 'The manifest could not be read.' };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return (_jsx(theme.Plugins, { description: `Installed for ${context.tenant ? context.tenant.name : 'this panel'}. A plugin runs on its own server; it sees what you grant it here and nothing else.`, allowed: allowed, review: reviewPlugin.bind(null, path), reviewing: reviewing, installed: installed.map((installation) => ({
|
|
304
|
+
id: installation.id,
|
|
305
|
+
label: installation.manifest.label,
|
|
306
|
+
description: installation.manifest.description ?? new URL(installation.manifest.endpoint).host,
|
|
307
|
+
resources: installation.manifest.resources.map((resource) => resource.label ?? humanize(resource.name)),
|
|
308
|
+
scopes: installation.manifest.scopes,
|
|
309
|
+
...(allowed ? { uninstall: uninstallPlugin.bind(null, path, installation.id) } : {}),
|
|
310
|
+
...(allowed && !installation.secretSeen
|
|
311
|
+
? { secret: { installation: installation.id, secret: installation.secret, copied: secretCopied.bind(null, path, installation.id) } }
|
|
312
|
+
: {}),
|
|
313
|
+
})) }));
|
|
314
|
+
}
|
|
315
|
+
// The pages read their url above everything they show, so each waits inside
|
|
316
|
+
// a boundary of its own: the build stores the theme's Loading as the page's
|
|
317
|
+
// shell, and a page that awaited params above every boundary would fail it.
|
|
318
|
+
// A panel cannot put a loading.tsx beside the app's route files; this is that.
|
|
319
|
+
function Dashboard() {
|
|
320
|
+
return (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "dashboard" }), children: _jsx(DashboardBody, {}) }));
|
|
321
|
+
}
|
|
322
|
+
function Index(props) {
|
|
323
|
+
return (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "table" }), children: _jsx(IndexBody, { ...props }) }));
|
|
324
|
+
}
|
|
325
|
+
function Create(props) {
|
|
326
|
+
return (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "form" }), children: _jsx(CreateBody, { ...props }) }));
|
|
327
|
+
}
|
|
328
|
+
function Edit(props) {
|
|
329
|
+
return (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "form" }), children: _jsx(EditBody, { ...props }) }));
|
|
330
|
+
}
|
|
331
|
+
function Plugins(props) {
|
|
332
|
+
return (_jsx(Suspense, { fallback: _jsx(theme.Loading, { kind: "form" }), children: _jsx(PluginsBody, { ...props }) }));
|
|
333
|
+
}
|
|
334
|
+
return { path, resources, middleware, Layout, Dashboard, Index, Table, Create, Edit, Plugins };
|
|
335
|
+
}
|
|
336
|
+
/** A cell: the column's own cell(), the theme's drawing of its format, or the value as text. */
|
|
337
|
+
async function draw(theme, column, value, row) {
|
|
338
|
+
if (column.cell)
|
|
339
|
+
return column.cell(value, row);
|
|
340
|
+
if (column.format === 'text' || column.format === 'relation')
|
|
341
|
+
return String(value ?? '');
|
|
342
|
+
return _jsx(theme.Cell, { format: column.format, value: value });
|
|
343
|
+
}
|
|
344
|
+
/** A value the browser can sort and search by. */
|
|
345
|
+
function plain(value) {
|
|
346
|
+
if (value === null || value === undefined)
|
|
347
|
+
return null;
|
|
348
|
+
if (value instanceof Date)
|
|
349
|
+
return value.toISOString();
|
|
350
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string')
|
|
351
|
+
return value;
|
|
352
|
+
return String(value);
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.js","sourceRoot":"","sources":["../../src/admin/panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Suspense } from 'react'\nimport type { ComponentType, ReactNode } from 'react'\nimport { notFound } from '@rsc-kit/core/not-found'\nimport { connection } from '@rsc-kit/core/request'\nimport { section } from '@rsc-kit/core/section'\nimport {\n bulkRecords,\n deleteRecord,\n installPlugin,\n reviewPlugin,\n saveRecord,\n secretCopied,\n signOut,\n switchTenant,\n uninstallPlugin,\n} from './actions.js'\nimport { digestOf, fetchManifest, RESERVED, scopeWords } from '../installable.js'\nimport type { Installs } from '../installable.js'\nimport type { PanelPlugin } from '../plugin.js'\nimport { allowOf, contextOf, registerPanel, resourcesFor } from './registry.js'\nimport type { PanelHandle, Tenancy } from './registry.js'\nimport { humanize } from '../resource.js'\nimport type { Column, Field, Option, Resource, SourceContext } from '../resource.js'\nimport type { AdminTheme, NavItem, PanelUser, PluginsProps, TableRowData } from './theme.js'\nimport type { ListSearch, PluginsSearch } from './url.js'\n\nexport { listSearch, pluginsSearch, recordParams, resourceParams } from './url.js'\n\nexport interface PanelDefinition {\n /** Where the panel's routes are: the folder under src/app it is mounted in. */\n path?: string\n title?: string\n /** The sidebar's mark beside the title, in a panel without tenants. */\n logo?: ComponentType<{ className?: string }>\n resources: Resource[]\n /**\n * What draws the panel: import { shadcn } from '@rsc-kit/press/shadcn', or\n * any object that fulfils AdminTheme. The panel decides what is shown and\n * who may see it; the theme only draws.\n */\n theme: AdminTheme\n /**\n * Whether this request may use the panel. Runs on every page and on every\n * save, delete and switch - an action is reachable without the page that\n * rendered it, so a guard on the pages alone would not be one. A refusal is\n * a 404. The panel has no login of its own: this asks the app's.\n */\n authorize?: () => boolean | Promise<boolean>\n /** Who is signed in, for the sidebar's footer. */\n user?: () => PanelUser | null | Promise<PanelUser | null>\n /** Sign the visitor out. Runs in a server action; the visitor lands on /. */\n signOut?: () => void | Promise<void>\n /** Whose records the panel shows: a switcher in the sidebar, and a tenant on every data source call. */\n tenancy?: Tenancy\n /** Packages that add resources, dashboard widgets and reactions to changes. */\n plugins?: PanelPlugin[]\n /**\n * Plugins a person installs from the panel while it runs, with no rebuild:\n * a manifest url, reviewed, then installed for the current tenant. Mounted\n * as src/app/admin/plugins/page.tsx. See installable.ts for what they can\n * and cannot reach.\n */\n installs?: Installs\n /** The light/dark switch in the header. On unless the app has its own. */\n modeToggle?: boolean\n}\n\n/** A type, not an interface: section() takes props that are a record, and an interface has no index signature. */\ntype Props<P, S = never> = {\n params: Promise<P>\n searchParams: Promise<S>\n}\n\n/**\n * A panel: its resources, and the pages that show them, already bound to it.\n * The app mounts each in a route file of its own, because the engine finds\n * routes, url schemas and sections by their files:\n *\n * src/app/admin/middleware.ts export default panel.middleware\n * src/app/admin/layout.tsx export default panel.Layout\n * src/app/admin/page.tsx export default panel.Dashboard\n * src/app/admin/[resource]/page.tsx export const params = resourceParams\n * export const searchParams = listSearch\n * export default panel.Index\n * src/app/admin/[resource]/admin-table.section.tsx export default panel.Table\n * src/app/admin/[resource]/new/page.tsx export const params = resourceParams\n * export default panel.Create\n * src/app/admin/[resource]/[id]/page.tsx export const params = recordParams\n * export default panel.Edit\n */\nexport function definePanel(definition: PanelDefinition) {\n const path = definition.path ?? '/admin'\n const title = definition.title ?? 'Admin'\n const plugins = definition.plugins ?? []\n const resources = [...definition.resources, ...plugins.flatMap((plugin) => plugin.resources ?? [])]\n const authorize = definition.authorize ?? (() => true)\n const tenancy = definition.tenancy ?? null\n const installs = definition.installs ?? null\n const theme = definition.theme\n /** The panel's own resources and its plugins'; any other is one a tenant installed. */\n const own = new Set(resources.map((resource) => resource.name))\n\n if (!/^(\\/[a-z0-9-]+)+$/.test(path)) {\n throw new Error(`[admin] a panel's path is where its routes are, with no trailing slash: ${JSON.stringify(path)}`)\n }\n\n const names = resources.map((resource) => resource.name)\n const twice = names.find((name, index) => names.indexOf(name) !== index)\n\n if (twice) throw new Error(`[admin] two resources are named ${twice}; a name is a url segment`)\n\n const reserved = installs ? names.find((name) => RESERVED.includes(name)) : undefined\n\n if (reserved) throw new Error(`[admin] a panel that takes installs has a plugins page, so no resource may be named ${reserved}`)\n\n const pluginNames = plugins.map((plugin) => plugin.name)\n const pluginTwice = pluginNames.find((name, index) => pluginNames.indexOf(name) !== index)\n\n if (pluginTwice) throw new Error(`[admin] the plugin ${pluginTwice} is installed twice`)\n\n // A relation names a resource by its url segment. Checked here, once every\n // resource is known, so a misspelt one fails at startup and not on the\n // first form that needs its choices.\n for (const resource of resources) {\n const named = [...resource.fields.map((field) => field.relation), ...resource.columns.map((column) => column.relation)]\n\n for (const relation of named) {\n if (relation !== undefined && !names.includes(relation)) {\n throw new Error(`[admin] ${resource.name} relates to ${relation}, which is not a resource of this panel`)\n }\n }\n }\n\n const handle: PanelHandle = { path, resources, authorize, plugins, tenancy, signOut: definition.signOut ?? null, installs }\n\n registerPanel(handle)\n\n /** A url of the panel. Typed routes are the app's; the panel's are its own. */\n const to = (...parts: string[]) => [path, ...parts].join('/')\n\n /**\n * A related resource's rows as choices: its key saved, its title shown.\n * Every row, scoped to the visitor's tenant like any list - so a belongsTo\n * is for a resource with a select's worth of rows: categories, teams,\n * plans. Memoised per render, so a field and a column naming the same\n * resource list it once.\n */\n function choices(context: SourceContext, resources: Resource[]) {\n const listed = new Map<string, Promise<Option[]>>()\n\n return (name: string): Promise<Option[]> => {\n let found = listed.get(name)\n\n if (!found) {\n const related = resources.find((resource) => resource.name === name)!\n\n found = related.source\n .list({ dir: 'asc', page: 1, perPage: null, filters: {}, searchable: [] }, context)\n .then(({ rows }) => rows.map((row) => ({ value: String(row[related.key]), label: String(row[related.title]) })))\n listed.set(name, found)\n }\n\n return found\n }\n }\n\n /** The form's fields, each relation given its choices. */\n async function fieldsFor(resource: Resource, context: SourceContext): Promise<Field[]> {\n const of = choices(context, await resourcesFor(handle, context))\n\n return Promise.all(resource.fields.map(async (field) => (field.relation ? { ...field, options: await of(field.relation) } : field)))\n }\n\n /**\n * The panel's door: a refusal is a 404, so the panel's existence is not\n * shown. Per request, always - authorize is about this visitor and the rows\n * are live - so the build stores the skeleton and never runs either.\n */\n async function enter(): Promise<SourceContext>\n async function enter(name: string): Promise<{ resource: Resource; context: SourceContext }>\n async function enter(name?: string) {\n await connection()\n\n if (!(await authorize())) notFound()\n\n const context = (await contextOf(handle)) ?? notFound()\n\n if (name === undefined) return context\n\n return { resource: (await resourcesFor(handle, context)).find((resource) => resource.name === name) ?? notFound(), context }\n }\n\n /**\n * The panel's guard, mounted as src/app/admin/middleware.ts. It runs before\n * anything is sent, so its refusals are real 404s: a visitor authorize turns\n * away, no tenant or another's, a resource the panel does not have, a record\n * that is not there - or not this tenant's. The pages check again, inside\n * their boundaries, where a refusal can no longer change the status.\n */\n async function middleware(params: Record<string, string | undefined> = {}) {\n if (!(await authorize())) notFound()\n\n const context = (await contextOf(handle)) ?? notFound()\n\n if (params.resource === undefined) return\n\n const resource = (await resourcesFor(handle, context)).find((candidate) => candidate.name === params.resource) ?? notFound()\n\n // An edit page changes a record. A read-only resource's new page cannot\n // be told apart here - it has the list's params - so it refuses itself.\n if (resource.readOnly && params.id !== undefined) notFound()\n\n if (params.id !== undefined && !(await resource.source.find(params.id, context))) notFound()\n }\n\n /** The sidebar's header: the tenant switcher, or the panel's name. */\n async function Tenants() {\n await connection()\n\n const context = (await authorize()) && tenancy ? await contextOf(handle) : null\n\n if (!tenancy || !context?.tenant) return brand()\n\n return (\n <theme.TenantSwitcher\n tenants={await tenancy.tenants()}\n current={context.tenant}\n label={tenancy.label ?? 'Teams'}\n choose={switchTenant.bind(null, path)}\n />\n )\n }\n\n function brand() {\n const Logo = definition.logo\n\n return <theme.Brand href={path} title={title} {...(Logo ? { logo: <Logo /> } : {})} />\n }\n\n async function User() {\n await connection()\n\n const user = (await authorize()) ? await definition.user?.() : null\n\n if (!user) return null\n\n return <theme.UserMenu user={user} {...(definition.signOut ? { signOut: signOut.bind(null, path) } : {})} />\n }\n\n /** A resource's own icon, drawn; the theme draws a default for one without. */\n function iconOf(resource: Resource) {\n const Icon = resource.icon\n\n return Icon ? { icon: <Icon /> } : {}\n }\n\n /** The sidebar's links: the panel's resources, then the tenant's installed ones, then Plugins. */\n function navOf(shown: Resource[]): NavItem[] {\n return [\n { href: path, label: 'Dashboard', kind: 'dashboard', exact: true },\n ...shown.map((resource): NavItem => ({\n href: to(resource.name),\n label: resource.label,\n kind: own.has(resource.name) ? 'resource' : 'installed',\n ...iconOf(resource),\n })),\n ...(installs ? [{ href: to('plugins'), label: 'Plugins', kind: 'plugins' } as const] : []),\n ]\n }\n\n /** Installed resources are this tenant's and this request's, so the nav waits for them; the panel's own show meanwhile. */\n async function InstalledNav() {\n await connection()\n\n const context = (await authorize()) ? await contextOf(handle) : null\n\n return <theme.Nav items={navOf(context ? await resourcesFor(handle, context) : resources)} />\n }\n\n function crumbsOf(shown: Resource[]) {\n return (\n <theme.Breadcrumbs\n root={path}\n title={title}\n labels={{ ...(installs ? { plugins: 'Plugins' } : {}), ...Object.fromEntries(shown.map((resource) => [resource.name, resource.label])) }}\n />\n )\n }\n\n async function InstalledCrumbs() {\n await connection()\n\n const context = (await authorize()) ? await contextOf(handle) : null\n\n return crumbsOf(context ? await resourcesFor(handle, context) : resources)\n }\n\n function Layout({ children }: { children: ReactNode }) {\n return (\n <theme.Shell\n title={title}\n modeToggle={definition.modeToggle !== false}\n brand={\n tenancy ? (\n <Suspense fallback={<theme.Loading kind=\"menu\" />}>\n <Tenants />\n </Suspense>\n ) : (\n brand()\n )\n }\n nav={\n installs ? (\n <Suspense fallback={<theme.Nav items={navOf(resources)} />}>\n <InstalledNav />\n </Suspense>\n ) : (\n <theme.Nav items={navOf(resources)} />\n )\n }\n user={\n definition.user ? (\n <Suspense fallback={<theme.Loading kind=\"menu\" />}>\n <User />\n </Suspense>\n ) : null\n }\n crumbs={\n installs ? (\n <Suspense fallback={crumbsOf(resources)}>\n <InstalledCrumbs />\n </Suspense>\n ) : (\n crumbsOf(resources)\n )\n }\n >\n {children}\n </theme.Shell>\n )\n }\n\n async function DashboardBody() {\n const context = await enter()\n const counts = await Promise.all(\n (await resourcesFor(handle, context)).map(async (resource) => {\n // An installed plugin is somebody else's server: one that is down\n // leaves its card blank, not the dashboard broken.\n const total = await resource.source\n .list({ dir: 'asc', page: 1, perPage: 1, filters: {}, searchable: [] }, context)\n .then(({ total: counted }) => counted as number | null)\n .catch((error: unknown) => {\n console.error(`[admin] counting ${resource.name} failed`, error)\n\n return null\n })\n\n return { name: resource.name, label: resource.label, href: to(resource.name), total, ...iconOf(resource) }\n }),\n )\n\n return (\n <theme.Dashboard\n description={context.tenant ? `Everything in ${context.tenant.name}.` : 'Everything in the panel, at a glance.'}\n counts={counts}\n widgets={plugins.flatMap((plugin) =>\n (plugin.widgets ?? []).map(({ title: widgetTitle, description, component: Widget }, index) => ({\n key: `${plugin.name}:${index}`,\n title: widgetTitle,\n ...(description ? { description } : {}),\n content: <Widget tenant={context.tenant} />,\n })),\n )}\n />\n )\n }\n\n /**\n * The table, as a section: a delete refreshes it and nothing else. A\n * refresh renders it with the page's params and searchParams, and nothing\n * else - which is why it is bound to the panel here rather than handed one\n * as a prop.\n */\n async function TableBody({ params, searchParams }: Props<{ resource: string }, ListSearch>) {\n const { resource, context } = await enter((await params).resource)\n const search = await searchParams\n const { q, sort, dir, page, ...rest } = search\n const filters: Record<string, string> = Object.fromEntries(\n Object.entries(rest).flatMap(([key, value]) =>\n typeof value === 'string' && value !== '' && resource.filters.some((filter) => filter.key === key) ? [[key, value]] : [],\n ),\n )\n const sortable = resource.columns.some((column) => column.sortable && column.key === sort) ? sort : undefined\n const server = resource.mode === 'server'\n const searchable = resource.columns.filter((column) => column.searchable).map((column) => column.key)\n const { rows, total } = await resource.source.list(\n server\n ? { ...(q ? { q } : {}), ...(sortable ? { sort: sortable } : {}), dir, page, perPage: resource.perPage, filters, searchable }\n : { dir: 'asc', page: 1, perPage: null, filters: {}, searchable },\n context,\n )\n\n // A relation column shows the related row's title, and is searched and\n // sorted by it in the browser too: what the visitor reads is what they type.\n const of = choices(context, await resourcesFor(handle, context))\n const titles = new Map(\n await Promise.all(\n resource.columns\n .filter((column) => column.relation !== undefined)\n .map(async (column) => [column.key, new Map((await of(column.relation!)).map((option) => [option.value, option.label]))] as const),\n ),\n )\n const shown = (column: Column, value: unknown) => {\n const title = value === null || value === undefined ? undefined : titles.get(column.key)?.get(String(value))\n\n return title ?? value\n }\n\n const drawn: TableRowData[] = await Promise.all(\n rows.map(async (row) => {\n const id = String(row[resource.key])\n\n return {\n id,\n cells: Object.fromEntries(\n await Promise.all(resource.columns.map(async (column) => [column.key, await draw(theme, column, shown(column, row[column.key]), row)] as const)),\n ),\n values: Object.fromEntries(resource.columns.map((column) => [column.key, plain(shown(column, row[column.key]))])),\n ...(resource.readOnly\n ? {}\n : { edit: to(resource.name, encodeURIComponent(id)), remove: deleteRecord.bind(null, path, resource.name, id) }),\n }\n }),\n )\n\n return (\n <theme.Table\n mode={resource.mode}\n href={to(resource.name)}\n columns={resource.columns.map(({ key, label, sortable: canSort, searchable: canSearch, visible }) => ({\n key,\n label,\n sortable: canSort,\n searchable: canSearch,\n visible,\n }))}\n rows={drawn}\n total={total}\n perPage={resource.perPage}\n query={{ q: q ?? '', ...(sortable ? { sort: sortable } : {}), dir, page, filters }}\n filters={resource.filters}\n noun={{ one: resource.singular, many: resource.label }}\n {...(resource.readOnly\n ? {}\n : {\n bulk: {\n actions: [\n ...resource.bulkActions.map(({ name, label, destructive }) => ({ name, label, destructive: destructive ?? false })),\n { name: 'delete', label: 'Delete', destructive: true },\n ],\n run: bulkRecords.bind(null, path, resource.name),\n },\n })}\n />\n )\n }\n\n const Table = section('admin-table', TableBody)\n\n async function IndexBody({ params, searchParams }: Props<{ resource: string }, ListSearch>) {\n const { resource } = await enter((await params).resource)\n\n return (\n <theme.Index\n title={resource.label}\n {...(resource.description ? { description: resource.description } : {})}\n {...(resource.readOnly ? {} : { create: { href: to(resource.name, 'new'), label: `New ${resource.singular.toLowerCase()}` } })}\n table={<Table params={params} searchParams={searchParams} />}\n />\n )\n }\n\n async function CreateBody({ params }: Props<{ resource: string }>) {\n const { resource, context } = await enter((await params).resource)\n\n if (resource.readOnly) notFound()\n\n return (\n <theme.Form\n title={`New ${resource.singular.toLowerCase()}`}\n description={`Add a ${resource.singular.toLowerCase()} to ${resource.label.toLowerCase()}.`}\n action={saveRecord.bind(null, path, resource.name, null)}\n fields={await fieldsFor(resource, context)}\n values={{}}\n submit={`Create ${resource.singular.toLowerCase()}`}\n cancel={to(resource.name)}\n />\n )\n }\n\n async function EditBody({ params }: Props<{ resource: string; id: string }>) {\n const { resource: name, id } = await params\n const { resource, context } = await enter(name)\n const record = resource.readOnly ? null : await resource.source.find(id, context)\n\n if (!record) notFound()\n\n return (\n <theme.Form\n title={String(record[resource.title])}\n description={`Edit this ${resource.singular.toLowerCase()}.`}\n action={saveRecord.bind(null, path, resource.name, id)}\n fields={await fieldsFor(resource, context)}\n values={record}\n submit=\"Save changes\"\n cancel={to(resource.name)}\n />\n )\n }\n\n /** The installed plugins, a manifest under review, and the form to start one. */\n async function PluginsBody({ searchParams }: { searchParams: Promise<PluginsSearch> }) {\n const context = await enter()\n\n if (!installs) notFound()\n\n const allowed = !installs.manage || (await installs.manage())\n const { review } = await searchParams\n const installed = await installs.store.list(context.tenant)\n const labelOf = (name: string) => resources.find((resource) => resource.name === name)?.label ?? name\n let reviewing: PluginsProps['reviewing'] = null\n\n // Once installed, the review is over: the install re-renders this url in place, ?review= and all.\n if (review && allowed && !installed.some((installation) => installation.manifestUrl === review)) {\n try {\n const manifest = await fetchManifest(review, allowOf(handle), resources.map((resource) => resource.name))\n\n reviewing = {\n label: manifest.label,\n ...(manifest.version ? { version: manifest.version } : {}),\n description: manifest.description ?? new URL(manifest.endpoint).host,\n adds: manifest.resources.map((resource) => resource.label ?? humanize(resource.name)),\n can: manifest.scopes.length === 0 ? [] : scopeWords(manifest, labelOf),\n endpoint: manifest.endpoint,\n install: installPlugin.bind(null, path, review, await digestOf(manifest)),\n cancel: to('plugins'),\n }\n } catch (error) {\n reviewing = { problem: error instanceof Error ? error.message : 'The manifest could not be read.' }\n }\n }\n\n return (\n <theme.Plugins\n description={`Installed for ${context.tenant ? context.tenant.name : 'this panel'}. A plugin runs on its own server; it sees what you grant it here and nothing else.`}\n allowed={allowed}\n review={reviewPlugin.bind(null, path)}\n reviewing={reviewing}\n installed={installed.map((installation) => ({\n id: installation.id,\n label: installation.manifest.label,\n description: installation.manifest.description ?? new URL(installation.manifest.endpoint).host,\n resources: installation.manifest.resources.map((resource) => resource.label ?? humanize(resource.name)),\n scopes: installation.manifest.scopes,\n ...(allowed ? { uninstall: uninstallPlugin.bind(null, path, installation.id) } : {}),\n ...(allowed && !installation.secretSeen\n ? { secret: { installation: installation.id, secret: installation.secret, copied: secretCopied.bind(null, path, installation.id) } }\n : {}),\n }))}\n />\n )\n }\n\n // The pages read their url above everything they show, so each waits inside\n // a boundary of its own: the build stores the theme's Loading as the page's\n // shell, and a page that awaited params above every boundary would fail it.\n // A panel cannot put a loading.tsx beside the app's route files; this is that.\n function Dashboard() {\n return (\n <Suspense fallback={<theme.Loading kind=\"dashboard\" />}>\n <DashboardBody />\n </Suspense>\n )\n }\n\n function Index(props: Props<{ resource: string }, ListSearch>) {\n return (\n <Suspense fallback={<theme.Loading kind=\"table\" />}>\n <IndexBody {...props} />\n </Suspense>\n )\n }\n\n function Create(props: Props<{ resource: string }>) {\n return (\n <Suspense fallback={<theme.Loading kind=\"form\" />}>\n <CreateBody {...props} />\n </Suspense>\n )\n }\n\n function Edit(props: Props<{ resource: string; id: string }>) {\n return (\n <Suspense fallback={<theme.Loading kind=\"form\" />}>\n <EditBody {...props} />\n </Suspense>\n )\n }\n\n function Plugins(props: { searchParams: Promise<PluginsSearch> }) {\n return (\n <Suspense fallback={<theme.Loading kind=\"form\" />}>\n <PluginsBody {...props} />\n </Suspense>\n )\n }\n\n return { path, resources, middleware, Layout, Dashboard, Index, Table, Create, Edit, Plugins }\n}\n\nexport type Panel = ReturnType<typeof definePanel>\n\n/** A cell: the column's own cell(), the theme's drawing of its format, or the value as text. */\nasync function draw(theme: AdminTheme, column: Column, value: unknown, row: unknown): Promise<ReactNode> {\n if (column.cell) return column.cell(value, row)\n if (column.format === 'text' || column.format === 'relation') return String(value ?? '')\n\n return <theme.Cell format={column.format} value={value} />\n}\n\n/** A value the browser can sort and search by. */\nfunction plain(value: unknown): string | number | boolean | null {\n if (value === null || value === undefined) return null\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value\n\n return String(value)\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Installs } from '../installable.js';
|
|
2
|
+
import type { PanelPlugin, RecordChange } from '../plugin.js';
|
|
3
|
+
import type { Resource, SourceContext, Tenant } from '../resource.js';
|
|
4
|
+
/**
|
|
5
|
+
* Tenants: whose records the panel is showing. The panel never stores which
|
|
6
|
+
* one is current - the app already knows, in its session, a cookie, the
|
|
7
|
+
* user's row - so it asks, and tells the app when the visitor switches.
|
|
8
|
+
*/
|
|
9
|
+
export interface Tenancy {
|
|
10
|
+
/** What a tenant is called in the switcher: 'Team', 'Store', 'Workspace'. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** The tenants this visitor belongs to. */
|
|
13
|
+
tenants: () => Tenant[] | Promise<Tenant[]>;
|
|
14
|
+
/** The one they are working in. Null - none chosen, or no longer theirs - is a 404. */
|
|
15
|
+
current: () => Tenant | null | Promise<Tenant | null>;
|
|
16
|
+
/** Remember the visitor's choice. Runs in a server action, so it may set a cookie. */
|
|
17
|
+
switch: (id: string) => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** What an action needs of a panel: its resources, who may use them, and whose rows they are. */
|
|
20
|
+
export interface PanelHandle {
|
|
21
|
+
path: string;
|
|
22
|
+
resources: Resource[];
|
|
23
|
+
authorize: () => boolean | Promise<boolean>;
|
|
24
|
+
plugins: PanelPlugin[];
|
|
25
|
+
tenancy: Tenancy | null;
|
|
26
|
+
signOut: (() => void | Promise<void>) | null;
|
|
27
|
+
/** Plugins installed while it runs, or null when the panel takes none. */
|
|
28
|
+
installs: Installs | null;
|
|
29
|
+
}
|
|
30
|
+
/** Where a panel may send a plugin's calls: the app's rule, or https to a public name. */
|
|
31
|
+
export declare function allowOf(panel: PanelHandle): (url: URL) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The panel's resources for this request: its own, then each installed
|
|
34
|
+
* plugin's for this tenant. A plugin whose manifest no longer fits - the app
|
|
35
|
+
* renamed a resource it reads, or added one with its name - is left out and
|
|
36
|
+
* said so, rather than failing every page.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resourcesFor(panel: PanelHandle, context: SourceContext): Promise<Resource[]>;
|
|
39
|
+
/**
|
|
40
|
+
* A change, told to the build-time plugins and to each installed plugin that
|
|
41
|
+
* asked for it. An installed plugin is told within three seconds or not at
|
|
42
|
+
* all: it is somebody else's server, and the person saving is not waiting
|
|
43
|
+
* for it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function tellAll(panel: PanelHandle, change: RecordChange, context: SourceContext): Promise<void>;
|
|
46
|
+
export declare function registerPanel(panel: PanelHandle): void;
|
|
47
|
+
export declare function panelAt(path: string): PanelHandle;
|
|
48
|
+
/**
|
|
49
|
+
* Who is asking, as a data source is told. In a panel with tenants, the
|
|
50
|
+
* current tenant must be one of the visitor's - an app whose current() reads
|
|
51
|
+
* a cookie would otherwise hand anyone the tenant the cookie names.
|
|
52
|
+
*/
|
|
53
|
+
export declare function contextOf(panel: PanelHandle): Promise<SourceContext | null>;
|
|
54
|
+
/** The resource and its context, once the visitor is allowed into the panel - and allowed to change it. */
|
|
55
|
+
export declare function allowedResource(path: string, name: string): Promise<{
|
|
56
|
+
resource: Resource;
|
|
57
|
+
context: SourceContext;
|
|
58
|
+
}>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { callPlugin, manifestProblems, publicHttps, resourcesOf, wantsChange } from '../installable.js';
|
|
2
|
+
import { tell } from '../plugin.js';
|
|
3
|
+
/** Where a panel may send a plugin's calls: the app's rule, or https to a public name. */
|
|
4
|
+
export function allowOf(panel) {
|
|
5
|
+
return panel.installs?.allow ?? publicHttps;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The panel's resources for this request: its own, then each installed
|
|
9
|
+
* plugin's for this tenant. A plugin whose manifest no longer fits - the app
|
|
10
|
+
* renamed a resource it reads, or added one with its name - is left out and
|
|
11
|
+
* said so, rather than failing every page.
|
|
12
|
+
*/
|
|
13
|
+
export async function resourcesFor(panel, context) {
|
|
14
|
+
if (!panel.installs)
|
|
15
|
+
return panel.resources;
|
|
16
|
+
const host = panel.resources.map((resource) => resource.name);
|
|
17
|
+
const installed = [];
|
|
18
|
+
const taken = new Set(host);
|
|
19
|
+
for (const installation of await panel.installs.store.list(context.tenant)) {
|
|
20
|
+
const problems = manifestProblems(installation.manifest, installation.manifestUrl, host);
|
|
21
|
+
const names = installation.manifest.resources.map((resource) => resource.name);
|
|
22
|
+
if (problems.length > 0 || names.some((name) => taken.has(name))) {
|
|
23
|
+
console.error(`[admin] the installed plugin ${installation.manifest.name} is left out: ${problems.join('; ') || 'a resource name is taken'}`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
names.forEach((name) => taken.add(name));
|
|
27
|
+
installed.push(...resourcesOf(installation, allowOf(panel)));
|
|
28
|
+
}
|
|
29
|
+
return [...panel.resources, ...installed];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A change, told to the build-time plugins and to each installed plugin that
|
|
33
|
+
* asked for it. An installed plugin is told within three seconds or not at
|
|
34
|
+
* all: it is somebody else's server, and the person saving is not waiting
|
|
35
|
+
* for it.
|
|
36
|
+
*/
|
|
37
|
+
export async function tellAll(panel, change, context) {
|
|
38
|
+
await tell(panel.plugins, change);
|
|
39
|
+
if (!panel.installs)
|
|
40
|
+
return;
|
|
41
|
+
const listening = (await panel.installs.store.list(context.tenant)).filter((installation) => wantsChange(installation, change));
|
|
42
|
+
await Promise.all(listening.map((installation) => callPlugin(installation, { type: 'change', change: { ...change, tenant: context.tenant } }, context, allowOf(panel), 3000).catch((error) => console.error(`[admin] the installed plugin ${installation.manifest.name} was not told of ${change.change} ${change.resource} ${change.id}`, error))));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Panels by path. An action receives a path, not a panel - only what can be
|
|
46
|
+
* serialised crosses to the browser and back - so the panel is looked up here.
|
|
47
|
+
* On globalThis because a dev server can evaluate this module more than once,
|
|
48
|
+
* and a second copy of the map would hold no panels.
|
|
49
|
+
*/
|
|
50
|
+
const KEY = Symbol.for('@rsc-kit/press.panels');
|
|
51
|
+
const panels = (globalThis[KEY] ??= new Map());
|
|
52
|
+
export function registerPanel(panel) {
|
|
53
|
+
panels.set(panel.path, panel);
|
|
54
|
+
}
|
|
55
|
+
export function panelAt(path) {
|
|
56
|
+
const panel = panels.get(path);
|
|
57
|
+
if (!panel)
|
|
58
|
+
throw new Error(`[admin] no panel at ${path}; definePanel({ path: ${JSON.stringify(path)} }) has not run`);
|
|
59
|
+
return panel;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Who is asking, as a data source is told. In a panel with tenants, the
|
|
63
|
+
* current tenant must be one of the visitor's - an app whose current() reads
|
|
64
|
+
* a cookie would otherwise hand anyone the tenant the cookie names.
|
|
65
|
+
*/
|
|
66
|
+
export async function contextOf(panel) {
|
|
67
|
+
if (!panel.tenancy)
|
|
68
|
+
return { tenant: null };
|
|
69
|
+
const tenant = await panel.tenancy.current();
|
|
70
|
+
if (!tenant)
|
|
71
|
+
return null;
|
|
72
|
+
const theirs = await panel.tenancy.tenants();
|
|
73
|
+
return theirs.some((candidate) => candidate.id === tenant.id) ? { tenant } : null;
|
|
74
|
+
}
|
|
75
|
+
/** The resource and its context, once the visitor is allowed into the panel - and allowed to change it. */
|
|
76
|
+
export async function allowedResource(path, name) {
|
|
77
|
+
const panel = panelAt(path);
|
|
78
|
+
if (!(await panel.authorize()))
|
|
79
|
+
throw new Error('[admin] not allowed');
|
|
80
|
+
const context = await contextOf(panel);
|
|
81
|
+
if (!context)
|
|
82
|
+
throw new Error('[admin] no tenant chosen, or not one of this visitor\'s');
|
|
83
|
+
const resource = (await resourcesFor(panel, context)).find((candidate) => candidate.name === name);
|
|
84
|
+
if (!resource)
|
|
85
|
+
throw new Error(`[admin] no resource ${name} in the panel at ${path}`);
|
|
86
|
+
if (resource.readOnly)
|
|
87
|
+
throw new Error(`[admin] ${name} is read-only`);
|
|
88
|
+
return { resource, context };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=registry.js.map
|