Admin UI
Plugins can extend the admin panel with custom pages and dashboard widgets. These are React components that render alongside core admin functionality.
Admin Entry Point
Section titled “Admin Entry Point”Plugins with admin UI export components from an admin entry point:
import { SEOSettingsPage } from "./components/SEOSettingsPage";import { SEODashboardWidget } from "./components/SEODashboardWidget";
// Dashboard widgetsexport const widgets = { "seo-overview": SEODashboardWidget,};
// Admin pagesexport const pages = { "/settings": SEOSettingsPage,};Configure the entry point in package.json:
{ "exports": { ".": "./dist/index.js", "./admin": "./dist/admin.js" }}Reference it in your plugin definition:
definePlugin({ id: "seo", version: "1.0.0",
admin: { entry: "@my-org/plugin-seo/admin", pages: [{ path: "/settings", label: "SEO Settings", icon: "settings" }], widgets: [{ id: "seo-overview", title: "SEO Overview", size: "half" }], },});Admin Pages
Section titled “Admin Pages”Admin pages are React components that receive the plugin context via hooks.
Page Definition
Section titled “Page Definition”Define pages in admin.pages:
admin: { pages: [ { path: "/settings", // URL path (relative to plugin base) label: "Settings", // Sidebar label icon: "settings", // Icon name (optional) }, { path: "/reports", label: "Reports", icon: "chart", }, ];}Pages mount at /_emdash/admin/plugins/<plugin-id>/<path>.
Page Component
Section titled “Page Component”import { useState, useEffect } from "react";import { usePluginAPI } from "@emdash-cms/admin";
export function SettingsPage() { const api = usePluginAPI(); const [settings, setSettings] = useState<Record<string, unknown>>({}); const [saving, setSaving] = useState(false);
useEffect(() => { api.get("settings").then(setSettings); }, []);
const handleSave = async () => { setSaving(true); await api.post("settings/save", settings); setSaving(false); };
return ( <div> <h1>Plugin Settings</h1>
<label> Site Title <input type="text" value={settings.siteTitle || ""} onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })} /> </label>
<label> <input type="checkbox" checked={settings.enabled ?? true} onChange={(e) => setSettings({ ...settings, enabled: e.target.checked })} /> Enabled </label>
<button onClick={handleSave} disabled={saving}> {saving ? "Saving..." : "Save Settings"} </button> </div> );}Plugin API Hook
Section titled “Plugin API Hook”Use usePluginAPI() to call your plugin’s routes:
import { usePluginAPI } from "@emdash-cms/admin";
function MyComponent() { const api = usePluginAPI();
// GET request to plugin route const data = await api.get("status");
// POST request with body await api.post("settings/save", { enabled: true });
// With URL parameters const result = await api.get("history?limit=50");}The hook automatically adds the plugin ID prefix to route URLs.
Dashboard Widgets
Section titled “Dashboard Widgets”Widgets appear on the admin dashboard and provide at-a-glance information.
Widget Definition
Section titled “Widget Definition”Define widgets in admin.widgets:
admin: { widgets: [ { id: "seo-overview", // Unique widget ID title: "SEO Overview", // Widget title (optional) size: "half", // "full" | "half" | "third" }, ];}Widget Component
Section titled “Widget Component”import { useState, useEffect } from "react";import { usePluginAPI } from "@emdash-cms/admin";
export function SEOWidget() { const api = usePluginAPI(); const [data, setData] = useState({ score: 0, issues: [] });
useEffect(() => { api.get("analyze").then(setData); }, []);
return ( <div className="widget-content"> <div className="score">{data.score}%</div> <ul> {data.issues.map((issue, i) => ( <li key={i}>{issue.message}</li> ))} </ul> </div> );}Widget Sizes
Section titled “Widget Sizes”| Size | Description |
|---|---|
full | Full dashboard width |
half | Half dashboard width |
third | One-third dashboard width |
Widgets wrap automatically based on screen width.
Export Structure
Section titled “Export Structure”The admin entry point exports two objects:
import { SettingsPage } from "./components/SettingsPage";import { ReportsPage } from "./components/ReportsPage";import { StatusWidget } from "./components/StatusWidget";import { OverviewWidget } from "./components/OverviewWidget";
// Pages keyed by pathexport const pages = { "/settings": SettingsPage, "/reports": ReportsPage,};
// Widgets keyed by IDexport const widgets = { status: StatusWidget, overview: OverviewWidget,};Using Admin Components
Section titled “Using Admin Components”EmDash provides pre-built components for common patterns:
import { Card, Button, Input, Select, Toggle, Table, Pagination, Alert, Loading} from "@emdash-cms/admin";
function SettingsPage() { return ( <Card title="Settings"> <Input label="API Key" type="password" /> <Toggle label="Enabled" defaultChecked /> <Button variant="primary">Save</Button> </Card> );}Auto-Generated Settings UI
Section titled “Auto-Generated Settings UI”If your plugin only needs a settings form, use admin.settingsSchema without custom components:
admin: { settingsSchema: { apiKey: { type: "secret", label: "API Key" }, enabled: { type: "boolean", label: "Enabled", default: true } }}EmDash generates a settings page automatically. Add custom pages only for functionality beyond basic settings.
Navigation
Section titled “Navigation”Plugin pages appear in the admin sidebar under the plugin name. The order matches the admin.pages array.
admin: { pages: [ { path: "/settings", label: "Settings", icon: "settings" }, // First { path: "/history", label: "History", icon: "history" }, // Second { path: "/reports", label: "Reports", icon: "chart" }, // Third ];}Build Configuration
Section titled “Build Configuration”Admin components need a separate build entry point. Configure your bundler:
export default { entry: { index: "src/index.ts", admin: "src/admin.tsx" }, format: "esm", dts: true, external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]};export default { entry: ["src/index.ts", "src/admin.tsx"], format: "esm", dts: true, external: ["react", "react-dom", "emdash", "@emdash-cms/admin"]};Keep React and EmDash admin as external dependencies to avoid bundling duplicates.
Plugin Enable/Disable
Section titled “Plugin Enable/Disable”When a plugin is disabled in the admin:
- Sidebar links are hidden
- Dashboard widgets are not rendered
- Admin pages return 404
- Backend hooks still execute (for data safety)
Plugins can check their enabled state:
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");Example: Complete Admin UI
Section titled “Example: Complete Admin UI”import { definePlugin } from "emdash";
export default definePlugin({ id: "analytics", version: "1.0.0",
capabilities: ["network:fetch"], allowedHosts: ["api.analytics.example.com"],
storage: { events: { indexes: ["type", "createdAt"] }, },
admin: { entry: "@my-org/plugin-analytics/admin", settingsSchema: { trackingId: { type: "string", label: "Tracking ID" }, enabled: { type: "boolean", label: "Enabled", default: true }, }, pages: [ { path: "/dashboard", label: "Dashboard", icon: "chart" }, { path: "/settings", label: "Settings", icon: "settings" }, ], widgets: [{ id: "events-today", title: "Events Today", size: "third" }], },
routes: { stats: { handler: async (ctx) => { const today = new Date().toISOString().split("T")[0]; const count = await ctx.storage.events!.count({ createdAt: { gte: today }, }); return { today: count }; }, }, },});import { EventsWidget } from "./components/EventsWidget";import { DashboardPage } from "./components/DashboardPage";import { SettingsPage } from "./components/SettingsPage";
export const widgets = { "events-today": EventsWidget,};
export const pages = { "/dashboard": DashboardPage, "/settings": SettingsPage,};