Menu API
Routes are added to the sidebar navigation via the @menu_entry decorator.
Basic usage
from fastpluggy.core.menu.decorator import menu_entry
@menu_entry(label="Dashboard", icon="ti ti-dashboard")
@router.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request): ...
Decorator parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
str |
required | Text shown in the sidebar. |
icon |
str |
"" |
Icon CSS classes, e.g. "ti ti-home" or "fas fa-chart-bar". |
parent |
str \| None |
None |
Name of a parent item to nest under. |
type |
str |
"main" |
Sidebar section: "main" or "admin". |
position |
int \| None |
None |
Sort order within the parent (lower = higher). |
divider_before |
bool |
False |
Render a separator line above this item. |
divider_after |
bool |
False |
Render a separator line below this item. |
required_role |
str \| None |
None |
Role the current user must hold for this item to render. Checked via the auth backend's user_has_role (see below). |
permission |
str \| None |
None |
Legacy permission string — not enforced by core; use required_role instead. |
Parent items
To group routes under a collapsible parent, create the parent in after_setup_templates
and reference it by name in @menu_entry(parent=...):
# plugin.py
def after_setup_templates(self, fast_pluggy):
fast_pluggy.menu_manager.create_parent_item(
name="my_plugin",
label="My Plugin",
icon="ti ti-star",
type="main",
position=20,
)
# router.py
@menu_entry(label="Overview", icon="ti ti-layout-dashboard", parent="my_plugin")
@router.get("/overview", response_class=HTMLResponse)
async def overview(request: Request): ...
@menu_entry(label="Reports", icon="ti ti-report", parent="my_plugin", position=2)
@router.get("/reports", response_class=HTMLResponse)
async def reports(request: Request): ...
Admin section
Use type="admin" to place the entry in the admin-only section (shown only to users with
is_admin = True, or when auth is disabled):
@menu_entry(label="Plugin Config", icon="ti ti-adjustments", type="admin")
@router.get("/config", response_class=HTMLResponse)
async def config(request: Request): ...
Role-gated menu items
Pass required_role to hide an item unless the current user holds that role. The
check runs for every rendered item via the auth backend's
user_has_role, so the policy lives in the
backend, not in the menu:
@menu_entry(label="My courses", icon="ti ti-chalkboard", required_role="teacher")
@router.get("/teacher/courses", response_class=HTMLResponse)
async def courses(request: Request): ...
required_role is a plain string label — a declarative "who is this for". The
default policy resolves it against request.auth.scopes plus a super-user
short-circuit: a user with is_admin (or any role in the backend's
super_user_roles, {"fp_admin", "admin"} by default) sees every item
regardless of required_role, so the admin console can never hide an item from
the admin who configures it. Apps needing role hierarchies, wildcards, or a
permission→role map override user_has_role — no menu changes required.
required_rolegates rendering, not access. Hiding the link does not protect the route — still gate the route itself withrequire_role(...)(see Auth). A link the user shouldn't reach should be both hidden and guarded.
permissionis a dead field — userequired_role. The olderpermissionparameter is carried throughto_dict()for backward compatibility but core never enforces it. New code should userequired_role.