Páginas admin e widgets React

Nesta página

Plugins nativos podem estender o painel admin com páginas React personalizadas e widgets do dashboard — plugins sandbox descrevem sua UI como Block Kit em vez disso, porque carregar JavaScript do plugin no admin quebraria o isolamento do sandbox.

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

Ponto de entrada admin

Plugins com UI admin 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 no package.json:

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

Referencie-o a partir 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 o EmDash saiba onde encontrar os componentes no momento da compilação:

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

Páginas admin

Páginas admin são componentes React montados em /_emdash/admin/plugins/<plugin-id>/<path>.

Definição de página

Declare cada página sob admin.pages com um caminho, rótulo e ícone:

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

Declare os rótulos em inglês. O admin os passa pela instância compartilhada do Lingui antes de renderizar a barra lateral e a paleta de comandos, então um plugin que carrega seu próprio catálogo de mensagens — com o rótulo em inglês como id da mensagem — obtém navegação localizada gratuitamente. Rótulos que correspondem a uma das mensagens próprias do admin (Settings, Dashboard, …) usam as traduções do admin mesmo sem um catálogo de plugin; rótulos sem entrada de catálogo são renderizados como declarados.

import { i18n } from "@lingui/core";

// Mesclar o catálogo compilado do plugin na instância i18n do admin.
// "Reports" agora é renderizado como "Berichte" quando o locale do admin é alemão.
const catalogs: Record<string, Record<string, string>> = {
	de: { Reports: "Berichte", Settings: "Einstellungen" },
};

function mergeCatalog() {
	const messages = catalogs[i18n.locale];
	if (messages && !("Reports" in i18n.messages)) i18n.load(i18n.locale, messages);
}

mergeCatalog();
// O seletor de locale do admin *substitui* o catálogo na mudança, então re-mesclar.
// A verificação sentinela acima evita a recursão (load() dispara "change").
i18n.on("change", mergeCatalog);

Componente de página

O seguinte componente lê e salva configurações através do hook de API do 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>Configurações do plugin</h1>

			<label>
				Título do site
				<input
					type="text"
					value={(settings.siteTitle as string) || ""}
					onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
				/>
			</label>

			<button onClick={handleSave} disabled={saving}>
				{saving ? "Salvando..." : "Salvar configurações"}
			</button>
		</div>
	);
}

Hook de API do plugin

usePluginAPI() chama as rotas do seu plugin com o prefixo do id do plugin e o header CSRF X-EmDash-Request: 1 adicionado 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 dashboard

Widgets aparecem no dashboard admin e fornecem informações rápidas.

Definição do widget

Declare cada widget sob admin.widgets com um id, título e tamanho:

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

Componente do widget

O seguinte componente busca seus dados ao montar e renderiza um resumo 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>
	);
}

Tamanhos de widget

TamanhoDescrição
fullLargura total do dashboard
halfMetade da largura do dashboard
thirdUm terço da largura do dashboard

Os widgets se ajustam automaticamente com base na largura da tela.

Estrutura de exportação

O ponto de entrada admin 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,
};

Usando componentes admin

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="Configurações">
			<Input label="Chave API" type="password" />
			<Toggle label="Ativado" defaultChecked />
			<Button variant="primary">Salvar</Button>
		</Card>
	);
}

UI de configurações auto-gerada

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

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

As páginas do plugin aparecem na barra lateral admin sob o nome do plugin. A ordem corresponde ao array admin.pages, como mostrado abaixo:

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // primeira
		{ path: "/history", label: "History", icon: "history" },     // segunda
		{ path: "/reports", label: "Reports", icon: "chart" },        // terceira
	],
}

Configuração de compilação

Componentes admin precisam de um ponto de entrada de compilação separado. A seguinte configuração do bundler compila os pontos de entrada do servidor e do admin:

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 duplicatas no bundle.

Ativar/desativar plugin

Quando um plugin é desativado no admin:

  • Links da barra lateral são ocultados.
  • Widgets do dashboard não são renderizados.
  • Páginas admin retornam 404.
  • Hooks do backend continuam executando (para segurança dos dados).

Os plugins podem verificar seu estado de ativação:

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

Exemplo completo

O seguinte plugin define uma página de dashboard, uma página de configurações e um widget, com os pontos de entrada de runtime e admin em arquivos separados. O arquivo src/index.ts contém o descritor e o runtime:

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;

O arquivo src/admin.tsx mapeia caminhos de página e ids de widget para seus 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,
};