Skip to content

Simplify approach for registering plugin navigation items #4401

@jeremystretch

Description

@jeremystretch

Proposed Changes

I'd like a to adopt a simpler mechanism than what currently exists in the 3351-plugins branch for plugins to register navigation menu items. We have a few options, which I'll outline here.

Current approach: Signals

The current approach requires three components. First, we declare the menu items in a file:

navigation.py

class NavLink1(PluginNavMenuLink):
    link = 'plugins:my_plugin:view1'
    link_text = 'Link A'

class NavLink2(PluginNavMenuLink):
    link = 'plugins:my_plugin:view2'
    link_text = 'Link B'

Next, we create a function to return these classes, and register it as a signal receiver:

signals.py

@receiver(register_nav_menu_link_classes)
def nav_menu_link_classes(**kwargs):
    return [NavLink1, NavLink2]

Finally, we extend our PluginApp to import this function on ready(). (We could probably build this part into the PluginApp class.)

init.py

class MyPluginConfig(PluginConfig):
    ...

    def ready(self):
        from . import signals

Proposal: Import of a known variable

The first option I propose is to direct plugin authors to define a single variable within a standard file, i.e. navigation.py, which contains all navigation menu items for their plugin. (This is very similar in concept to the way a plugin currently declares URL patterns in urls.py.)

navigation.py

class NavLink1(PluginNavMenuLink):
    link = 'plugins:my_plugin:view1'
    link_text = 'Link A'

class NavLink2(PluginNavMenuLink):
    link = 'plugins:my_plugin:view2'
    link_text = 'Link B'

menu_items = (NavLink1, NavLink2)

NetBox would automatically import and register the classes in menu_items (if defined by the plugin). This could be handled by the PluginConfig's ready() method, like how we're currently handling signals.

With a minor tweak to the PluginNavMenuLink class to accept attributes on initialization, the above example could be further simplified to skip naming the individual instances:

navigation.py

menu_items = (
    PluginNavMenuLink(
        link='plugins:my_plugin:view1',
        link_text='Link A'
    ),
    PluginNavMenuLink(
        link='plugins:my_plugin:view2',
        link_text='Link B'
    ),
)

Justification

I prefer this approach for four reasons:

  1. It minimizes the amount of code needed from the plugin author.
  2. It is much easier to understand what's happening, especially for someone unfamiliar with Django signals.
  3. It provides the simplest possible integration point: A single variable.
  4. This approach is already well-established by Django's URL patterns mechanism (wherein each app provides a urlpatterns variable).

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: acceptedThis issue has been accepted for implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions