Les plugins natifs peuvent étendre le panneau admin avec des pages React personnalisées et des widgets de tableau de bord — les plugins sandboxés décrivent leur UI en Block Kit à la place, car charger du JavaScript de plugin dans l’admin briserait l’isolation du sandbox.
Si votre plugin n’a besoin que d’un formulaire de paramètres, le formulaire auto-généré admin.settingsSchema (voir Votre premier plugin natif) couvre la plupart des cas sans écrire de React. N’utilisez des composants personnalisés que lorsque vous avez besoin d’une UI plus riche que ce que settingsSchema fournit.
Point d’entrée admin
Les plugins avec une UI admin exportent les objets pages et widgets depuis un point d’entrée admin :
import { SEOSettingsPage } from "./components/SEOSettingsPage";
import { SEODashboardWidget } from "./components/SEODashboardWidget";
export const widgets = {
"seo-overview": SEODashboardWidget,
};
export const pages = {
"/settings": SEOSettingsPage,
};
Configurez le point d’entrée dans package.json :
{
"exports": {
".": "./dist/index.js",
"./admin": "./dist/admin.js"
}
}
Référencez-le depuis 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" }],
},
});
Le descripteur nécessite un adminEntry correspondant pour qu’EmDash sache où trouver les composants au moment de la compilation :
adminEntry: "@my-org/plugin-seo/admin",
Pages admin
Les pages admin sont des composants React montés sous /_emdash/admin/plugins/<plugin-id>/<path>.
Définition de page
Déclarez chaque page sous admin.pages avec un chemin, un libellé et une icône :
admin: {
pages: [
{
path: "/settings",
label: "Settings",
icon: "settings",
},
{
path: "/reports",
label: "Reports",
icon: "chart",
},
],
}
Déclarez les libellés en anglais. L’admin les passe par son instance partagée de Lingui avant de rendre la barre latérale et la palette de commandes, donc un plugin qui charge son propre catalogue de messages — avec le libellé anglais comme id de message — obtient la navigation localisée gratuitement. Les libellés qui correspondent à l’un des messages propres de l’admin (Settings, Dashboard, …) reprennent les traductions de l’admin même sans catalogue de plugin ; les libellés sans entrée de catalogue sont rendus tels que déclarés.
import { i18n } from "@lingui/core";
// Fusionner le catalogue compilé du plugin dans l'instance i18n de l'admin.
// "Reports" s'affiche maintenant comme "Berichte" quand la locale de l'admin est l'allemand.
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();
// Le sélecteur de locale de l'admin *remplace* le catalogue lors du changement, donc re-fusionner.
// La vérification sentinelle ci-dessus empêche la récursion (load() déclenche "change").
i18n.on("change", mergeCatalog);
Composant de page
Le composant suivant lit et enregistre les paramètres via le hook d’API du 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>Paramètres du plugin</h1>
<label>
Titre du site
<input
type="text"
value={(settings.siteTitle as string) || ""}
onChange={(e) => setSettings({ ...settings, siteTitle: e.target.value })}
/>
</label>
<button onClick={handleSave} disabled={saving}>
{saving ? "Enregistrement..." : "Enregistrer les paramètres"}
</button>
</div>
);
}
Hook d’API du plugin
usePluginAPI() appelle les routes de votre plugin avec le préfixe d’id du plugin et l’en-tête CSRF X-EmDash-Request: 1 ajouté automatiquement :
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 avec corps JSON
const result = await api.get("history?limit=50"); // paramètres de requête supportés
}
Widgets du tableau de bord
Les widgets apparaissent sur le tableau de bord admin et fournissent des informations en un coup d’œil.
Définition du widget
Déclarez chaque widget sous admin.widgets avec un id, un titre et une taille :
admin: {
widgets: [
{
id: "seo-overview",
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
],
}
Composant du widget
Le composant suivant récupère ses données au montage et rend un résumé compact :
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>
);
}
Tailles de widget
| Taille | Description |
|---|---|
full | Pleine largeur du tableau de bord |
half | Demi-largeur du tableau de bord |
third | Un tiers de la largeur du tableau de bord |
Les widgets s’enveloppent automatiquement en fonction de la largeur de l’écran.
Structure d’exportation
Le point d’entrée admin exporte deux objets :
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,
};
Utiliser les composants admin
EmDash fournit des composants préconstruits pour les modèles courants :
import {
Card,
Button,
Input,
Select,
Toggle,
Table,
Pagination,
Alert,
Loading,
} from "@emdash-cms/admin";
function SettingsPage() {
return (
<Card title="Paramètres">
<Input label="Clé API" type="password" />
<Toggle label="Activé" defaultChecked />
<Button variant="primary">Enregistrer</Button>
</Card>
);
}
UI de paramètres auto-générée
Si votre plugin n’a besoin que d’un formulaire de paramètres, utilisez admin.settingsSchema sans composants personnalisés :
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
},
},
EmDash génère automatiquement une page de paramètres. N’utilisez des pages React personnalisées que lorsque vous avez besoin d’un comportement au-delà d’un formulaire basique.
Navigation
Les pages du plugin apparaissent dans la barre latérale admin sous le nom du plugin. L’ordre correspond au tableau admin.pages, comme montré ci-dessous :
admin: {
pages: [
{ path: "/settings", label: "Settings", icon: "settings" }, // première
{ path: "/history", label: "History", icon: "history" }, // deuxième
{ path: "/reports", label: "Reports", icon: "chart" }, // troisième
],
}
Configuration de compilation
Les composants admin nécessitent un point d’entrée de compilation séparé. La configuration de bundler suivante compile les points d’entrée serveur et 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"],
}; Gardez React et EmDash admin comme dépendances externes pour éviter les doublons dans le bundle.
Activation/désactivation du plugin
Lorsqu’un plugin est désactivé dans l’admin :
- Les liens de la barre latérale sont masqués.
- Les widgets du tableau de bord ne sont pas rendus.
- Les pages admin retournent 404.
- Les hooks du backend continuent de s’exécuter (pour la sécurité des données).
Les plugins peuvent vérifier leur état d’activation :
const enabled = await ctx.kv.get<boolean>("_emdash:enabled");
Exemple complet
Le plugin suivant définit une page de tableau de bord, une page de paramètres et un widget, avec les points d’entrée runtime et admin dans des fichiers séparés. Le fichier src/index.ts contient le descripteur et le 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;
Le fichier src/admin.tsx mappe les chemins de page et les ids de widget à leurs composants 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,
};