Pagine di amministrazione e widget React

In questa pagina

I plugin nativi possono estendere il pannello di amministrazione con pagine React personalizzate e widget della dashboard — i plugin sandboxed descrivono invece la loro interfaccia utente come Block Kit, perché l’invio di JavaScript del plugin nell’amministratore violerebbe l’isolamento della sandbox.

Se il tuo plugin necessita solo di un modulo di impostazioni, il modulo admin.settingsSchema generato automaticamente (vedi Il tuo primo plugin nativo) copre la maggior parte dei casi senza scrivere React. Ricorri ai componenti personalizzati quando hai bisogno di un’interfaccia utente più ricca di quella fornita da settingsSchema.

Punto di ingresso dell’amministrazione

I plugin con interfaccia utente di amministrazione esportano 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"
	}
}

Fai riferimento 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 in modo che EmDash sappia dove trovare i componenti al momento della compilazione:

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

Pagine di amministrazione

Le pagine di amministrazione sono componenti React che vengono 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",
		},
	],
}

Componente della pagina

Il seguente componente legge e salva le impostazioni tramite l’hook dell’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 dell’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 with JSON body
	const result = await api.get("history?limit=50");            // query params supported
}

Widget della dashboard

I widget appaiono sulla dashboard di amministrazione 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 rende 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 dei widget

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

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

Struttura di esportazione

Il punto di ingresso dell’amministrazione 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,
};

Utilizzo dei componenti di amministrazione

EmDash fornisce componenti predefiniti per pattern comuni:

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

Interfaccia utente delle impostazioni auto-generata

Se il tuo plugin necessita 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. Ricorri alle pagine React personalizzate solo quando hai bisogno di un comportamento oltre un modulo di base.

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

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

Configurazione di build

I componenti di amministrazione necessitano di un punto di ingresso di build separato. La seguente configurazione del bundler compila sia il server che i punti di ingresso di amministrazione:

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 di impacchettare duplicati.

Abilitare/disabilitare il plugin

Quando un plugin è disabilitato nell’amministrazione:

  • I collegamenti della barra laterale sono nascosti.
  • I widget della dashboard non vengono renderizzati.
  • Le pagine di amministrazione restituiscono 404.
  • Gli hook del backend vengono comunque eseguiti (per la sicurezza dei dati).

I plugin possono verificare il loro stato abilitato:

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 di runtime e amministrazione 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,
};