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
| Dimensione | Descrizione |
|---|---|
full | Larghezza completa della dashboard |
half | Metà larghezza della dashboard |
third | Un 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.
Navigazione
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,
};