Skip to content

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_role gates rendering, not access. Hiding the link does not protect the route — still gate the route itself with require_role(...) (see Auth). A link the user shouldn't reach should be both hidden and guarded.

permission is a dead field — use required_role. The older permission parameter is carried through to_dict() for backward compatibility but core never enforces it. New code should use required_role.