Páginas de administración y widgets de React

En esta página

Los plugins nativos pueden extender el panel de administración con páginas React personalizadas y widgets del panel de control — los plugins en sandbox describen su interfaz de usuario como Block Kit en su lugar, porque enviar JavaScript del plugin al administrador rompería el aislamiento del sandbox.

Si su plugin solo necesita un formulario de configuración, el formulario admin.settingsSchema generado automáticamente (consulte Su primer plugin nativo) cubre la mayoría de los casos sin escribir ningún React. Recurra a componentes personalizados cuando necesite una interfaz de usuario más rica que la que proporciona settingsSchema.

Punto de entrada de administración

Los plugins con interfaz de usuario 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,
};

Configure el punto de entrada en package.json:

{
	"exports": {
		".": "./dist/index.js",
		"./admin": "./dist/admin.js"
	}
}

Referencie 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

Declare cada página bajo admin.pages con una ruta, etiqueta e icono:

admin: {
	pages: [
		{
			path: "/settings",
			label: "Settings",
			icon: "settings",
		},
		{
			path: "/reports",
			label: "Reports",
			icon: "chart",
		},
	],
}

Componente de página

El siguiente componente lee y guarda la configuración a través del hook de la API del plugin:

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 la API del plugin

usePluginAPI() llama a las rutas de su plugin con el prefijo del 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 with JSON body
	const result = await api.get("history?limit=50");            // query params supported
}

Widgets del panel de control

Los widgets aparecen en el panel de control de administración y proporcionan información de un vistazo.

Definición de widget

Declare cada widget bajo admin.widgets con un id, título y tamaño:

admin: {
	widgets: [
		{
			id: "seo-overview",
			title: "SEO Overview",
			size: "half",   // "full" | "half" | "third"
		},
	],
}

Componente de widget

El siguiente componente recupera sus datos al montarse y renderiza un resumen compacto:

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 prediseñados 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>
	);
}

Interfaz de usuario de configuración autogenerada

Si su plugin solo necesita un formulario de configuración, use admin.settingsSchema sin componentes personalizados:

admin: {
	settingsSchema: {
		apiKey: { type: "secret", label: "API Key" },
		enabled: { type: "boolean", label: "Enabled", default: true },
	},
},

EmDash genera automáticamente una página de configuración. Recurra a páginas React personalizadas solo cuando necesite un 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, como se muestra a continuación:

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // first
		{ path: "/history", label: "History", icon: "history" },     // second
		{ path: "/reports", label: "Reports", icon: "chart" },        // third
	],
}

Configuración de compilación

Los componentes de administración necesitan un punto de entrada de compilación separado. La siguiente configuración del empaquetador compila tanto el servidor como los puntos de entrada de administración:

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

Mantenga React y EmDash admin como dependencias externas para evitar empaquetar duplicados.

Habilitar/deshabilitar plugin

Cuando un plugin está deshabilitado en el administrador:

  • Los enlaces de la barra lateral están ocultos.
  • Los widgets del panel de control no se renderizan.
  • Las páginas de administración devuelven 404.
  • Los hooks del backend aún se ejecutan (por seguridad de los datos).

Los plugins pueden verificar su estado habilitado:

const enabled = await ctx.kv.get<boolean>("_emdash:enabled");

Ejemplo completo

El siguiente plugin define una página de panel de control, una página de configuración y un widget, con los puntos de entrada de tiempo de ejecución y administración en archivos separados. El archivo src/index.ts contiene el descriptor y el tiempo de ejecución:

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;

El archivo src/admin.tsx mapea rutas de página e IDs de widget a sus componentes React:

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