Páginas de administración y widgets React

En esta página

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

SizeDescription
fullAncho completo del panel
halfMitad del ancho del panel
thirdUn 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.

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,
};