@zerotal/admin 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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/package.json +78 -0
- package/src/Cluster.ts +50 -0
- package/src/Panel.ts +288 -0
- package/src/PanelInstance.ts +644 -0
- package/src/Resource.ts +918 -0
- package/src/actions/Action.ts +607 -0
- package/src/actions/ImportRecordsJob.ts +108 -0
- package/src/actions/csv.ts +123 -0
- package/src/actions/index.ts +39 -0
- package/src/actions/render.tsx +181 -0
- package/src/actions/transfer.ts +307 -0
- package/src/actions/xlsx.ts +304 -0
- package/src/auth/AuthLayout.tsx +34 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/pages/ForgotPasswordPage.tsx +87 -0
- package/src/auth/pages/LoginPage.tsx +121 -0
- package/src/auth/pages/ProfilePage.tsx +216 -0
- package/src/auth/pages/ResetPasswordPage.tsx +103 -0
- package/src/auth/pages/VerifyEmailPage.tsx +68 -0
- package/src/auth/register.ts +44 -0
- package/src/authRoles.ts +141 -0
- package/src/commands/MakeAdminResourceCommand.ts +181 -0
- package/src/config.ts +128 -0
- package/src/dashboardLayout.ts +101 -0
- package/src/databaseMedia.ts +148 -0
- package/src/databaseNotifications.ts +169 -0
- package/src/form/Field.ts +928 -0
- package/src/form/ResourceForm.ts +48 -0
- package/src/form/Section.ts +364 -0
- package/src/form/editors.ts +43 -0
- package/src/form/index.ts +59 -0
- package/src/history.ts +151 -0
- package/src/impersonation.ts +126 -0
- package/src/index.ts +380 -0
- package/src/infolist/Entry.ts +537 -0
- package/src/infolist/Section.ts +99 -0
- package/src/infolist/index.ts +38 -0
- package/src/media.ts +297 -0
- package/src/notifications.ts +65 -0
- package/src/pages/AdminPage.ts +100 -0
- package/src/pages/ConsolePage.tsx +324 -0
- package/src/pages/DashboardPage.tsx +264 -0
- package/src/pages/MediaPage.tsx +346 -0
- package/src/pages/NotificationsPage.tsx +155 -0
- package/src/pages/RecordViewPage.tsx +951 -0
- package/src/pages/ResourceFormPage.tsx +1856 -0
- package/src/pages/ResourceListPage.tsx +2552 -0
- package/src/pages/RolesPage.tsx +325 -0
- package/src/pages/SearchPage.tsx +169 -0
- package/src/plugin.ts +283 -0
- package/src/provider/AdminAbilityMiddleware.ts +25 -0
- package/src/provider/AdminGuardMiddleware.ts +29 -0
- package/src/provider/AdminProvider.ts +334 -0
- package/src/relations/RelationManager.ts +114 -0
- package/src/renderHooks.ts +86 -0
- package/src/roles.ts +175 -0
- package/src/savedViews.ts +79 -0
- package/src/support/ability.ts +73 -0
- package/src/support/authorize.ts +105 -0
- package/src/support/countCache.ts +37 -0
- package/src/support/hostPage.ts +30 -0
- package/src/table/Column.ts +353 -0
- package/src/table/Constraint.ts +238 -0
- package/src/table/Filter.ts +275 -0
- package/src/table/Group.ts +73 -0
- package/src/table/Tab.ts +77 -0
- package/src/testing.ts +121 -0
- package/src/theme.ts +70 -0
- package/src/ui/AdminLayout.tsx +355 -0
- package/src/ui/Breadcrumbs.tsx +84 -0
- package/src/ui/environmentIndicator.tsx +63 -0
- package/src/ui/icons.tsx +124 -0
- package/src/widgets/Widget.ts +251 -0
- package/src/widgets/render.tsx +154 -0
package/src/Panel.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel — the registry of admin panels, and the facade over the one most
|
|
3
|
+
* applications have:
|
|
4
|
+
*
|
|
5
|
+
* Panel.configure({ path: "/admin", brand: "Acme" });
|
|
6
|
+
* Panel.register(UserResource, PostResource);
|
|
7
|
+
* Panel.pages(ReportsPage);
|
|
8
|
+
*
|
|
9
|
+
* Every static here forwards to {@link Panel.current}, which is the panel whose
|
|
10
|
+
* path prefixes the request being served, and the default panel everywhere else
|
|
11
|
+
* (boot, CLI, tests). An app with a single panel therefore never has to think
|
|
12
|
+
* about instances at all.
|
|
13
|
+
*
|
|
14
|
+
* A second audience gets a second panel:
|
|
15
|
+
*
|
|
16
|
+
* const app = Panel.make("app", { path: "/app", brand: "Acme Console" });
|
|
17
|
+
* app.register(InvoiceResource);
|
|
18
|
+
*
|
|
19
|
+
* Each instance owns its resources, pages, widgets, guard and URL prefix, so the
|
|
20
|
+
* two never see each other's registrations.
|
|
21
|
+
*
|
|
22
|
+
* Packages don't call any of this directly. They push into the host bound as
|
|
23
|
+
* `admin.panel` — see `plugin.ts` for that side of the contract.
|
|
24
|
+
*/
|
|
25
|
+
import { RequestContext } from "@zerotal/core";
|
|
26
|
+
import type { AdminConfigShape, AdminAuthConfig } from "./config.ts";
|
|
27
|
+
import type { DashboardWidget } from "./widgets/Widget.ts";
|
|
28
|
+
import type { NotificationProvider } from "./notifications.ts";
|
|
29
|
+
import type { AdminPageClass } from "./pages/AdminPage.ts";
|
|
30
|
+
import type { RenderHook, RenderHookName } from "./renderHooks.ts";
|
|
31
|
+
import type { SavedViewProvider } from "./savedViews.ts";
|
|
32
|
+
import type { MediaProvider } from "./media.ts";
|
|
33
|
+
import type { RoleProvider } from "./roles.ts";
|
|
34
|
+
import type { DashboardLayoutStore } from "./dashboardLayout.ts";
|
|
35
|
+
import { PanelInstance } from "./PanelInstance.ts";
|
|
36
|
+
import type { NavGroup, PanelPage, ResourceClass } from "./PanelInstance.ts";
|
|
37
|
+
import type {
|
|
38
|
+
AdminPanelHost,
|
|
39
|
+
AdminPlugin,
|
|
40
|
+
ConsoleContribution,
|
|
41
|
+
PanelSearchProvider,
|
|
42
|
+
TopbarSlot,
|
|
43
|
+
UserMenuContribution,
|
|
44
|
+
WidgetContribution,
|
|
45
|
+
} from "./plugin.ts";
|
|
46
|
+
|
|
47
|
+
export { PanelInstance } from "./PanelInstance.ts";
|
|
48
|
+
export type { ResourceClass, NavItem, NavGroup, PanelPage } from "./PanelInstance.ts";
|
|
49
|
+
|
|
50
|
+
/** The id given to the panel every application starts with. */
|
|
51
|
+
export const DEFAULT_PANEL_ID = "admin";
|
|
52
|
+
|
|
53
|
+
export class Panel {
|
|
54
|
+
private static _panels = new Map<string, PanelInstance>();
|
|
55
|
+
|
|
56
|
+
// ── The registry ─────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** The panel every app starts with, created on first use. */
|
|
59
|
+
static default(): PanelInstance {
|
|
60
|
+
let panel = this._panels.get(DEFAULT_PANEL_ID);
|
|
61
|
+
if (!panel) {
|
|
62
|
+
panel = new PanelInstance(DEFAULT_PANEL_ID);
|
|
63
|
+
this._panels.set(DEFAULT_PANEL_ID, panel);
|
|
64
|
+
}
|
|
65
|
+
return panel;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create (or reconfigure) an additional panel. Give it a path of its own —
|
|
70
|
+
* two panels sharing a prefix cannot be told apart from a URL.
|
|
71
|
+
*/
|
|
72
|
+
static make(id: string, config: Partial<AdminConfigShape> = {}): PanelInstance {
|
|
73
|
+
const existing = this._panels.get(id);
|
|
74
|
+
if (existing) {
|
|
75
|
+
existing.configure(config);
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
const panel = new PanelInstance(id, config);
|
|
79
|
+
this._panels.set(id, panel);
|
|
80
|
+
return panel;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Resolve a panel by id. */
|
|
84
|
+
static get(id: string): PanelInstance | undefined {
|
|
85
|
+
return this._panels.get(id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Every registered panel, the default one first. */
|
|
89
|
+
static all(): PanelInstance[] {
|
|
90
|
+
this.default();
|
|
91
|
+
return [...this._panels.values()];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The panel the current request belongs to — the one whose path is the longest
|
|
96
|
+
* matching prefix of the request URL. Off-request (boot, CLI, queue workers,
|
|
97
|
+
* WebSocket actions) this is the default panel, which is also the right answer
|
|
98
|
+
* for the overwhelmingly common single-panel app.
|
|
99
|
+
*
|
|
100
|
+
* Pages generated for a specific panel hold that instance directly rather than
|
|
101
|
+
* asking here, so their WebSocket actions stay on the right panel too.
|
|
102
|
+
*/
|
|
103
|
+
static current(): PanelInstance {
|
|
104
|
+
const url = RequestContext.tryGet()?.request.url;
|
|
105
|
+
if (!url || this._panels.size < 2) return this.default();
|
|
106
|
+
let path: string;
|
|
107
|
+
try {
|
|
108
|
+
path = new URL(url).pathname;
|
|
109
|
+
} catch {
|
|
110
|
+
return this.default();
|
|
111
|
+
}
|
|
112
|
+
return this.forPath(path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The panel owning `path`, or the default panel when none claims it. */
|
|
116
|
+
static forPath(path: string): PanelInstance {
|
|
117
|
+
let best: PanelInstance | undefined;
|
|
118
|
+
for (const panel of this._panels.values()) {
|
|
119
|
+
const base = panel.base();
|
|
120
|
+
if (path !== base && !path.startsWith(`${base}/`)) continue;
|
|
121
|
+
if (!best || base.length > best.base().length) best = panel;
|
|
122
|
+
}
|
|
123
|
+
return best ?? this.default();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Forget every panel (tests). */
|
|
127
|
+
static reset(): void {
|
|
128
|
+
this._panels.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Facade over the current panel ────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
static configure(config: Partial<AdminConfigShape>): void {
|
|
134
|
+
this.current().configure(config);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static config(): AdminConfigShape {
|
|
138
|
+
return this.current().config();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static register(...resources: ResourceClass[]): void {
|
|
142
|
+
this.current().register(...resources);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static resources(): ResourceClass[] {
|
|
146
|
+
return this.current().resources();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static widgets(...widgets: DashboardWidget[]): void {
|
|
150
|
+
this.current().widgets(...widgets);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static dashboardWidgets(): DashboardWidget[] {
|
|
154
|
+
return this.current().dashboardWidgets();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static notifications(provider: NotificationProvider): void {
|
|
158
|
+
this.current().notifications(provider);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static notificationProvider(): NotificationProvider | undefined {
|
|
162
|
+
return this.current().notificationProvider();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static auth(config: AdminAuthConfig): void {
|
|
166
|
+
this.current().auth(config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static authConfig(): AdminAuthConfig | undefined {
|
|
170
|
+
return this.current().authConfig();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static find(slug: string): ResourceClass | undefined {
|
|
174
|
+
return this.current().find(slug);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static pages(...pages: AdminPageClass[]): void {
|
|
178
|
+
this.current().pages(...pages);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static registeredPages(): PanelPage[] {
|
|
182
|
+
return this.current().registeredPages();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static findPage(slug: string): PanelPage | undefined {
|
|
186
|
+
return this.current().findPage(slug);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static plugin(...plugins: AdminPlugin[]): Promise<void> {
|
|
190
|
+
return this.current().plugin(...plugins);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static pluginEnabled(id: string): boolean {
|
|
194
|
+
return this.current().pluginEnabled(id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static host(): AdminPanelHost {
|
|
198
|
+
return this.current().host();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static consoles(): ConsoleContribution[] {
|
|
202
|
+
return this.current().consoles();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static findConsole(slug: string): ConsoleContribution | undefined {
|
|
206
|
+
return this.current().findConsole(slug);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static contributedWidgets(): WidgetContribution[] {
|
|
210
|
+
return this.current().contributedWidgets();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static visibleWidgets(): Promise<DashboardWidget[]> {
|
|
214
|
+
return this.current().visibleWidgets();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static visibleSearchProviders(): Promise<PanelSearchProvider[]> {
|
|
218
|
+
return this.current().visibleSearchProviders();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static visibleTopbarSlots(): Promise<TopbarSlot[]> {
|
|
222
|
+
return this.current().visibleTopbarSlots();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static visibleUserMenuItems(): Promise<UserMenuContribution[]> {
|
|
226
|
+
return this.current().visibleUserMenuItems();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static savedViews(provider: SavedViewProvider): void {
|
|
230
|
+
this.current().savedViews(provider);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static savedViewProvider(): SavedViewProvider | undefined {
|
|
234
|
+
return this.current().savedViewProvider();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static media(provider: MediaProvider, options: { disk?: string } = {}): void {
|
|
238
|
+
this.current().media(provider, options);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static mediaDisk(): string | undefined {
|
|
242
|
+
return this.current().mediaDisk();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static mediaProvider(): MediaProvider | undefined {
|
|
246
|
+
return this.current().mediaProvider();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static roles(provider: RoleProvider): void {
|
|
250
|
+
this.current().roles(provider);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
static roleProvider(): RoleProvider | undefined {
|
|
254
|
+
return this.current().roleProvider();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static dashboardLayout(store: DashboardLayoutStore): void {
|
|
258
|
+
this.current().dashboardLayout(store);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
static dashboardLayoutStore(): DashboardLayoutStore | undefined {
|
|
262
|
+
return this.current().dashboardLayoutStore();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
static renderHook(name: RenderHookName, hook: RenderHook): void {
|
|
266
|
+
this.current().renderHook(name, hook);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static renderHooks(name: RenderHookName): RenderHook[] {
|
|
270
|
+
return this.current().renderHooks(name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
static can(ability: string | undefined): Promise<boolean> {
|
|
274
|
+
return this.current().can(ability);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
static navigationBadges(): Promise<Record<string, { text: string; color: string }>> {
|
|
278
|
+
return this.current().navigationBadges();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static navigation(): NavGroup[] {
|
|
282
|
+
return this.current().navigation();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static visibleNavigation(): Promise<NavGroup[]> {
|
|
286
|
+
return this.current().visibleNavigation();
|
|
287
|
+
}
|
|
288
|
+
}
|