Páginas de administração React e widgets

Nesta página

Plugins nativos podem estender o painel de administração com páginas React personalizadas e widgets do painel — plugins sandbox descrevem sua UI como Block Kit em vez disso, porque enviar JavaScript do plugin para o admin quebraria o isolamento do sandbox.

Se o seu plugin precisa apenas de um formulário de configurações, o formulário admin.settingsSchema gerado automaticamente (veja Seu primeiro plugin nativo) cobre a maioria dos casos sem escrever nenhum React. Recorra a componentes personalizados quando precisar de uma UI mais rica do que settingsSchema fornece.

Ponto de entrada da administração

Plugins com UI de administração exportam objetos pages e widgets de um ponto 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 o ponto de entrada em package.json:

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

Referencie-o de 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" }],
	},
});

O descritor precisa de um adminEntry correspondente para que EmDash saiba onde encontrar os componentes no momento da compilação:

adminEntry: "@my-org/plugin-seo/admin",

Páginas de administração

Páginas de administração são componentes React que são montados sob /_emdash/admin/plugins/<plugin-id>/<path>.

Definição 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 da API do plugin

usePluginAPI() chama as rotas do seu plugin com o prefixo do ID do plugin e o cabeçalho CSRF X-EmDash-Request: 1 adicionados automaticamente:

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 com corpo JSON
	const result = await api.get("history?limit=50");            // parâmetros de consulta suportados
}

Widgets do painel

Widgets aparecem no painel de administração e fornecem informações rápidas.

Definição 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>
	);
}

Tamanhos de widget

SizeDescription
fullLargura completa do painel
halfMetade da largura do painel
thirdUm terço da largura do painel

Widgets se ajustam automaticamente com base na largura da tela.

Estrutura de exportação

O ponto de entrada da administração exporta dois 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 administração

EmDash fornece componentes pré-construídos para padrões comuns:

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 configurações gerada automaticamente

Se o seu plugin precisa apenas de um formulário de configurações, use admin.settingsSchema sem componentes personalizados:

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

EmDash gera uma página de configurações automaticamente. Recorra a páginas React personalizadas apenas quando precisar de comportamento além de um formulário básico.

Páginas de plugin aparecem na barra lateral de administração sob o nome do plugin. A ordem corresponde ao array admin.pages.

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // primeiro
		{ path: "/history", label: "History", icon: "history" },     // segundo
		{ path: "/reports", label: "Reports", icon: "chart" },        // terceiro
	],
}

Configuração de compilação

Componentes de administração precisam de um ponto de entrada de compilação separado. Configure seu 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"],
};

Mantenha React e EmDash Admin como dependências externas para evitar empacotamento de duplicatas.

Ativar/desativar plugin

Quando um plugin está desativado na administração:

  • Links da barra lateral ficam ocultos.
  • Widgets do painel não são renderizados.
  • Páginas de administração retornam 404.
  • Hooks do backend ainda são executados (para segurança de dados).

Plugins podem verificar seu estado ativado:

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

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