Los plugins nativos pueden extender el panel de administración con páginas React personalizadas y widgets del panel — los plugins sandbox describen su UI como Block Kit en su lugar, porque enviar JavaScript del plugin al admin rompería el aislamiento del sandbox.
Si tu plugin solo necesita un formulario de configuración, el formulario admin.settingsSchema generado automáticamente (ver Tu primer plugin nativo) cubre la mayoría de los casos sin escribir ningún React. Recurre a componentes personalizados cuando necesites una UI más rica de lo que settingsSchema proporciona.
Punto de entrada de administración
Los plugins con UI de administración exportan objetos pages y widgets desde un punto de entrada admin:
import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";
export const widgets = {
"seo-overview": SEODashboardWidget,
};
export const pages = {
"/settings": SEOSettingsPage,
};
Configura el punto de entrada en package.json:
{
"exports": {
".": "./dist/index.js",
"./admin": "./dist/admin.js"
}
}
Referéncialo desde definePlugin():
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" }],
},
});
El descriptor necesita un adminEntry coincidente para que EmDash sepa dónde encontrar los componentes en tiempo de compilación:
adminEntry: "@my-org/plugin-seo/admin",
Páginas de administración
Las páginas de administración son componentes React que se montan bajo /_emdash/admin/plugins/<plugin-id>/<path>.
Definición de página
admin: {
pages: [
{
path: "/settings",
label: "Settings",
icon: "settings",
},
{
path: "/reports",
label: "Reports",
icon: "chart",
},
],
}
Componente de página
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 as string) || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save Settings"}
</button>
</div>
);
}
Hook de API del plugin
usePluginAPI() llama a las rutas de tu plugin con el prefijo de ID del plugin y el encabezado CSRF X-EmDash-Request: 1 añadido automáticamente:
import { usePluginAPI } from "@emdash-cms/admin";
function MyComponent() {
const api = usePluginAPI();
const data = await api.get("status"); // GET /_emdash/api/plugins/<id>/status
await api.post("settings/save", { enabled: true }); // POST con cuerpo JSON
const result = await api.get("history?limit=50"); // parámetros de consulta soportados
}
Widgets del panel
Los widgets aparecen en el panel de administración y proporcionan información de un vistazo.
Definición de widget
admin: {
widgets: [
{
id: "seo-overview",
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
],
}
Componente de widget
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 as { message: string }).message}</li>
))}
</ul>
</div>
);
}
Tamaños de widget
| Size | Description |
|---|---|
full | Ancho completo del panel |
half | Mitad del ancho del panel |
third | Un tercio del ancho del panel |
Los widgets se ajustan automáticamente según el ancho de la pantalla.
Estructura de exportación
El punto de entrada de administración exporta dos objetos:
import { SettingsPage } from "./components/SettingsPage";
import { ReportsPage } from "./components/ReportsPage";
import { StatusWidget } from "./components/StatusWidget";
import { OverviewWidget } from "./components/OverviewWidget";
export const pages = {
"/settings": SettingsPage,
"/reports": ReportsPage,
};
export const widgets = {
status: StatusWidget,
overview: OverviewWidget,
};
Uso de componentes de administración
EmDash proporciona componentes predefinidos para patrones comunes:
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>
);
}
UI de configuración generada automáticamente
Si tu plugin solo necesita un formulario de configuración, usa admin.settingsSchema sin componentes personalizados:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
},
EmDash genera una página de configuración automáticamente. Recurre a páginas React personalizadas solo cuando necesites comportamiento más allá de un formulario básico.
Navegación
Las páginas del plugin aparecen en la barra lateral de administración bajo el nombre del plugin. El orden coincide con el array admin.pages.
admin: {
pages: [
{ path: "/settings", label: "Settings", icon: "settings" }, // primero
{ path: "/history", label: "History", icon: "history" }, // segundo
{ path: "/reports", label: "Reports", icon: "chart" }, // tercero
],
}
Configuración de compilación
Los componentes de administración necesitan un punto de entrada de compilación separado. Configura tu bundler:
tsdown
export default {
entry: {
index: "src/index.ts",
admin: "src/admin.tsx",
},
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; tsup
export default {
entry: ["src/index.ts", "src/admin.tsx"],
format: "esm",
dts: true,
external: ["react", "react-dom", "emdash", "@emdash-cms/admin"],
}; Mantén React y EmDash Admin como dependencias externas para evitar empaquetar duplicados.
Activar/desactivar plugin
Cuando un plugin está desactivado en el admin:
- Los enlaces de la barra lateral están ocultos.
- Los widgets del panel no se renderizan.
- Las páginas de administración devuelven 404.
- Los hooks del backend aún se ejecutan (para seguridad de datos).
Los plugins pueden verificar su estado habilitado:
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");
Ejemplo completo
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export function analyticsPlugin(): PluginDescriptor {
return {
id: "analytics",
version: "1.0.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [
{ path: "/dashboard", label: "Dashboard", icon: "chart" },
{ path: "/settings", label: "Settings", icon: "settings" },
],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin() {
return definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["network:request"],
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 };
},
},
},
});
}
export default createPlugin;
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,
};