.skills/discourse-admin-ui/references/plugin-admin-interfaces.md
Use this reference when creating or reviewing a custom plugin admin UI under /admin/plugins/:plugin.
Build a plugin admin UI when a plugin needs more than a generated settings list: record management, setup workflows, reports, nested pages, custom tables, or multiple related configuration views. If the plugin only exposes site settings, rely on the generated settings page.
Core examples to inspect:
plugins/discourse-aiplugins/discourse-data-explorerplugins/discourse-chat-integrationplugins/discourse-gamificationUse add_admin_route in plugin.rb and pass use_new_show_route: true so the plugin uses the modern plugin show page:
add_admin_route("plugin_example.title", "plugin-example", { use_new_show_route: true })
Some plugins pass the options hash as the third argument; others use keyword style. Follow nearby style.
Examples:
plugins/discourse-ai/plugin.rbplugins/discourse-data-explorer/plugin.rbplugins/discourse-chat-integration/plugin.rbPlugin admin routes should attach to admin.adminPlugins.show with path: "/plugins".
Create a route map such as assets/javascripts/discourse/admin-plugin-example-plugin-route-map.js:
export default {
resource: "admin.adminPlugins.show",
path: "/plugins",
map() {
this.route("plugin-example-items", { path: "items" }, function () {
this.route("new");
this.route("edit", { path: "/:id/edit" });
});
},
};
Current examples:
plugins/discourse-ai/assets/javascripts/discourse/admin-discourse-ai-plugin-route-map.jsplugins/discourse-data-explorer/assets/javascripts/discourse/explorer-route-map.jsplugins/discourse-chat-integration/assets/javascripts/discourse/admin-chat-integration-plugin-route-map.jsModern plugin admin route JS files live under:
admin/assets/javascripts/discourse/routes/admin-plugins/show/
Templates live under:
admin/assets/javascripts/discourse/templates/admin-plugins/show/
Simple routes usually have one template like:
admin/assets/javascripts/discourse/templates/admin-plugins/show/plugin-example-items.gjs
Nested routes use the normal Ember structure with index.gjs, new.gjs, edit.gjs, or route-specific names under the route folder.
Register plugin configuration navigation from an initializer, and only run it for admins:
import { withPluginApi } from "discourse/lib/plugin-api";
const PLUGIN_ID = "plugin-example";
export default {
name: "plugin-example-admin-plugin-configuration-nav",
initialize(container) {
const currentUser = container.lookup("service:current-user");
if (!currentUser?.admin) {
return;
}
withPluginApi((api) => {
api.addAdminPluginConfigurationNav(PLUGIN_ID, [
{
label: "plugin_example.items.short_title",
route: "adminPlugins.show.plugin-example-items",
description: "plugin_example.items.description",
},
]);
});
},
};
The plugin's generated settings link is added automatically. Do not duplicate it in addAdminPluginConfigurationNav.
Top tabs are preferred for plugin navigation. Avoid introducing an inner sidebar for new plugin UIs.
Example: plugins/discourse-ai/assets/javascripts/discourse/initializers/admin-plugin-configuration-nav.js.
The plugin show wrapper renders the main DPageHeader; plugin index routes should normally render DPageSubheader to describe the selected tab and provide tab-local actions.
Use the same content primitives as core admin pages:
AdminConfigAreaCard for grouped forms and setup areas.AdminConfigAreaEmptyList for empty createable lists.d-table classes for growing record lists.new and edit routes for forms.BackButton on new/edit pages instead of the full header.Examples:
plugins/discourse-data-explorer/admin/assets/javascripts/admin/templates/admin-plugins/show/explorer/index.gjsplugins/discourse-chat-integration/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-chat-integration-providers/show.gjsplugins/discourse-ai/admin/assets/javascripts/discourse/templates/admin-plugins/show/discourse-ai-features/edit.gjsIf a plugin needs actions in the main plugin page header rather than inside a tab/subsection, render a component into the admin-plugin-config-page-actions outlet from the same initializer that registers plugin navigation.
api.renderInOutlet(
"admin-plugin-config-page-actions",
PluginExampleAdminActions
);
The outlet receives the plugin model and DPageHeader action components through outletArgs; use those instead of hand-rolling header buttons.
Implementation anchor: frontend/discourse/admin/components/admin-plugin-config-page.gjs.
use_new_show_route: true.admin.adminPlugins.show.admin/assets/javascripts/discourse/....api.addAdminPluginConfigurationNav in an admin-only initializer.DPageSubheader inside plugin index routes.new and edit.bin/lint --fix on changed files.