Pagine admin e widget React

In questa pagina

I plugin nativi possono estendere il pannello admin con pagine React personalizzate e widget della dashboard — i plugin sandbox descrivono la loro UI come Block Kit invece, perché caricare JavaScript del plugin nell’admin comprometterebbe l’isolamento del sandbox.

Se il tuo plugin ha bisogno solo di un modulo di impostazioni, il modulo auto-generato admin.settingsSchema (vedi Il tuo primo plugin nativo) copre la maggior parte dei casi senza scrivere React. Usa componenti personalizzati solo quando hai bisogno di un’UI più ricca di quella che settingsSchema fornisce.

Punto di ingresso admin

I plugin con UI admin esportano gli oggetti pages e widgets da un punto di ingresso admin:

import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";

export const widgets = {
	"seo-overview": SEODashboardWidget,
};

export const pages = {
	"/settings": SEOSettingsPage,
};

Configura il punto di ingresso in package.json:

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

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

Il descrittore necessita di un adminEntry corrispondente affinché EmDash sappia dove trovare i componenti al momento della compilazione:

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

Pagine admin

Le pagine admin sono componenti React montati sotto /_emdash/admin/plugins/<plugin-id>/<path>.

Definizione della pagina

Dichiara ogni pagina sotto admin.pages con un percorso, un’etichetta e un’icona:

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

Dichiara le etichette in inglese. L’admin le passa attraverso la sua istanza condivisa di Lingui prima di renderizzare la barra laterale e la palette dei comandi, quindi un plugin che carica il proprio catalogo di messaggi — con l’etichetta inglese come id del messaggio — ottiene la navigazione localizzata gratuitamente. Le etichette che corrispondono a uno dei messaggi propri dell’admin (Settings, Dashboard, …) utilizzano le traduzioni dell’admin anche senza un catalogo del plugin; le etichette senza voce di catalogo vengono renderizzate come dichiarate.

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

// Unisci il catalogo compilato del plugin nell'istanza i18n dell'admin.
// "Reports" ora viene renderizzato come "Berichte" quando la locale dell'admin è tedesco.
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();
// Il selettore di locale dell'admin *sostituisce* il catalogo al cambio, quindi ri-unisci.
// Il controllo sentinella sopra impedisce la ricorsione (load() attiva "change").
i18n.on("change", mergeCatalog);

Componente della pagina

Il seguente componente legge e salva le impostazioni tramite l’hook 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>Impostazioni del plugin</h1>

			<label>
				Titolo del sito
				<input
					type="text"
					value={(settings.siteTitle as string) || ""}
					onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
				/>
			</label>

			<button onClick={handleSave} disabled={saving}>
				{saving ? "Salvataggio..." : "Salva impostazioni"}
			</button>
		</div>
	);
}

Hook API del plugin

usePluginAPI() chiama le route del tuo plugin con il prefisso dell’id del plugin e l’header CSRF X-EmDash-Request: 1 aggiunto 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 con corpo JSON
	const result = await api.get("history?limit=50");            // parametri di query supportati
}

Widget della dashboard

I widget appaiono sulla dashboard dell’admin e forniscono informazioni a colpo d’occhio.

Definizione del widget

Dichiara ogni widget sotto admin.widgets con un id, un titolo e una dimensione:

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

Componente del widget

Il seguente componente recupera i suoi dati al montaggio e renderizza un riepilogo compatto:

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

Dimensioni del widget

DimensioneDescrizione
fullLarghezza completa della dashboard
halfMetà larghezza della dashboard
thirdUn terzo della larghezza della dashboard

I widget si avvolgono automaticamente in base alla larghezza dello schermo.

Struttura di esportazione

Il punto di ingresso admin esporta due oggetti:

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

Usare i componenti admin

EmDash fornisce componenti precostituiti per i pattern comuni:

import {
	Card,
	Button,
	Input,
	Select,
	Toggle,
	Table,
	Pagination,
	Alert,
	Loading,
} from "@emdash-cms/admin";

function SettingsPage() {
	return (
		<Card title="Impostazioni">
			<Input label="Chiave API" type="password" />
			<Toggle label="Abilitato" defaultChecked />
			<Button variant="primary">Salva</Button>
		</Card>
	);
}

UI delle impostazioni auto-generata

Se il tuo plugin ha bisogno solo di un modulo di impostazioni, usa admin.settingsSchema senza componenti personalizzati:

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

EmDash genera automaticamente una pagina di impostazioni. Usa pagine React personalizzate solo quando hai bisogno di un comportamento oltre un modulo basico.

Le pagine del plugin appaiono nella barra laterale admin sotto il nome del plugin. L’ordine corrisponde all’array admin.pages, come mostrato di seguito:

admin: {
	pages: [
		{ path: "/settings", label: "Settings", icon: "settings" },  // prima
		{ path: "/history", label: "History", icon: "history" },     // seconda
		{ path: "/reports", label: "Reports", icon: "chart" },        // terza
	],
}

Configurazione della compilazione

I componenti admin necessitano di un punto di ingresso di compilazione separato. La seguente configurazione del bundler compila sia i punti di ingresso del server che dell’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"],
};

Mantieni React ed EmDash admin come dipendenze esterne per evitare duplicati nel bundle.

Attivazione/disattivazione del plugin

Quando un plugin è disattivato nell’admin:

  • I link della barra laterale sono nascosti.
  • I widget della dashboard non vengono renderizzati.
  • Le pagine admin restituiscono 404.
  • Gli hook del backend continuano ad eseguirsi (per la sicurezza dei dati).

I plugin possono verificare il loro stato di attivazione:

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

Esempio completo

Il seguente plugin definisce una pagina dashboard, una pagina di impostazioni e un widget, con i punti di ingresso runtime e admin in file separati. Il file src/index.ts contiene il descrittore e il 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;

Il file src/admin.tsx mappa i percorsi delle pagine e gli id dei widget ai loro componenti 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,
};