mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Enable plugins to create root-level navigation menus
This commit is contained in:
@ -6,15 +6,16 @@ from django.apps import AppConfig
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
|
||||||
from extras.registry import registry
|
|
||||||
from utilities.choices import ButtonColorChoices
|
|
||||||
|
|
||||||
from extras.plugins.utils import import_object
|
from extras.plugins.utils import import_object
|
||||||
|
from extras.registry import registry
|
||||||
|
from netbox.navigation import MenuGroup
|
||||||
|
from utilities.choices import ButtonColorChoices
|
||||||
|
|
||||||
|
|
||||||
# Initialize plugin registry
|
# Initialize plugin registry
|
||||||
registry['plugins'] = {
|
registry['plugins'] = {
|
||||||
'graphql_schemas': [],
|
'graphql_schemas': [],
|
||||||
|
'menus': [],
|
||||||
'menu_items': {},
|
'menu_items': {},
|
||||||
'preferences': {},
|
'preferences': {},
|
||||||
'template_extensions': collections.defaultdict(list),
|
'template_extensions': collections.defaultdict(list),
|
||||||
@ -57,8 +58,8 @@ class PluginConfig(AppConfig):
|
|||||||
# Default integration paths. Plugin authors can override these to customize the paths to
|
# Default integration paths. Plugin authors can override these to customize the paths to
|
||||||
# integrated components.
|
# integrated components.
|
||||||
graphql_schema = 'graphql.schema'
|
graphql_schema = 'graphql.schema'
|
||||||
|
menu = 'navigation.menu'
|
||||||
menu_items = 'navigation.menu_items'
|
menu_items = 'navigation.menu_items'
|
||||||
menu_header = 'navigation.menu_heading'
|
|
||||||
template_extensions = 'template_content.template_extensions'
|
template_extensions = 'template_content.template_extensions'
|
||||||
user_preferences = 'preferences.preferences'
|
user_preferences = 'preferences.preferences'
|
||||||
|
|
||||||
@ -70,15 +71,11 @@ class PluginConfig(AppConfig):
|
|||||||
if template_extensions is not None:
|
if template_extensions is not None:
|
||||||
register_template_extensions(template_extensions)
|
register_template_extensions(template_extensions)
|
||||||
|
|
||||||
# Register navigation menu items (if defined)
|
# Register navigation menu or menu items (if defined)
|
||||||
try:
|
if menu := import_object(f"{self.__module__}.{self.menu}"):
|
||||||
menu_header = import_object(f"{self.__module__}.{self.menu_header}")
|
register_menu(menu)
|
||||||
except AttributeError:
|
if menu_items := import_object(f"{self.__module__}.{self.menu_items}"):
|
||||||
menu_header = None
|
register_menu_items(self.verbose_name, menu_items)
|
||||||
|
|
||||||
menu_items = import_object(f"{self.__module__}.{self.menu_items}")
|
|
||||||
if menu_items is not None:
|
|
||||||
register_menu_items(self.verbose_name, menu_header, menu_items)
|
|
||||||
|
|
||||||
# Register GraphQL schema (if defined)
|
# Register GraphQL schema (if defined)
|
||||||
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
|
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
|
||||||
@ -206,6 +203,22 @@ def register_template_extensions(class_list):
|
|||||||
# Navigation menu links
|
# Navigation menu links
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class PluginMenu:
|
||||||
|
icon = 'mdi-puzzle'
|
||||||
|
|
||||||
|
def __init__(self, label, groups, icon=None):
|
||||||
|
self.label = label
|
||||||
|
self.groups = [
|
||||||
|
MenuGroup(label, items) for label, items in groups
|
||||||
|
]
|
||||||
|
if icon is not None:
|
||||||
|
self.icon = icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon_class(self):
|
||||||
|
return f'mdi {self.icon}'
|
||||||
|
|
||||||
|
|
||||||
class PluginMenuItem:
|
class PluginMenuItem:
|
||||||
"""
|
"""
|
||||||
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
||||||
@ -252,7 +265,13 @@ class PluginMenuButton:
|
|||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
|
|
||||||
def register_menu_items(section_name, menu_header, class_list):
|
def register_menu(menu):
|
||||||
|
if not isinstance(menu, PluginMenu):
|
||||||
|
raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
|
||||||
|
registry['plugins']['menus'].append(menu)
|
||||||
|
|
||||||
|
|
||||||
|
def register_menu_items(section_name, class_list):
|
||||||
"""
|
"""
|
||||||
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
|
||||||
"""
|
"""
|
||||||
@ -264,9 +283,7 @@ def register_menu_items(section_name, menu_header, class_list):
|
|||||||
if not isinstance(button, PluginMenuButton):
|
if not isinstance(button, PluginMenuButton):
|
||||||
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
|
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
|
||||||
|
|
||||||
registry['plugins']['menu_items'][section_name] = {}
|
registry['plugins']['menu_items'][section_name] = class_list
|
||||||
registry['plugins']['menu_items'][section_name]['header'] = menu_header
|
|
||||||
registry['plugins']['menu_items'][section_name]['items'] = class_list
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -63,7 +63,7 @@ class PluginTest(TestCase):
|
|||||||
Check that plugin MenuItems and MenuButtons are registered.
|
Check that plugin MenuItems and MenuButtons are registered.
|
||||||
"""
|
"""
|
||||||
self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
|
self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
|
||||||
menu_items = registry['plugins']['menu_items']['Dummy plugin']['items']
|
menu_items = registry['plugins']['menu_items']['Dummy plugin']
|
||||||
self.assertEqual(len(menu_items), 2)
|
self.assertEqual(len(menu_items), 2)
|
||||||
self.assertEqual(len(menu_items[0].buttons), 2)
|
self.assertEqual(len(menu_items[0].buttons), 2)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
from .navigation import *
|
from . import *
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -324,31 +324,19 @@ MENUS = [
|
|||||||
# Add plugin menus
|
# Add plugin menus
|
||||||
#
|
#
|
||||||
|
|
||||||
|
for menu in registry['plugins']['menus']:
|
||||||
|
MENUS.append(menu)
|
||||||
|
|
||||||
if registry['plugins']['menu_items']:
|
if registry['plugins']['menu_items']:
|
||||||
plugin_menu_groups = []
|
|
||||||
|
|
||||||
for plugin_name, data in registry['plugins']['menu_items'].items():
|
# Build the default plugins menu
|
||||||
if data['header']:
|
groups = [
|
||||||
menu_groups = [MenuGroup(label=plugin_name, items=data["items"])]
|
MenuGroup(label=label, items=items)
|
||||||
icon = data["header"]["icon"]
|
for label, items in registry['plugins']['menu_items'].items()
|
||||||
MENUS.append(Menu(
|
]
|
||||||
label=data["header"]["title"],
|
plugins_menu = Menu(
|
||||||
icon_class=f"mdi {icon}",
|
label="Plugins",
|
||||||
groups=menu_groups
|
icon_class="mdi mdi-puzzle",
|
||||||
))
|
groups=groups
|
||||||
else:
|
)
|
||||||
plugin_menu_groups.append(
|
MENUS.append(plugins_menu)
|
||||||
MenuGroup(
|
|
||||||
label=plugin_name,
|
|
||||||
items=data["items"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if plugin_menu_groups:
|
|
||||||
PLUGIN_MENU = Menu(
|
|
||||||
label="Plugins",
|
|
||||||
icon_class="mdi mdi-puzzle",
|
|
||||||
groups=plugin_menu_groups
|
|
||||||
)
|
|
||||||
|
|
||||||
MENUS.append(PLUGIN_MENU)
|
|
||||||
|
Reference in New Issue
Block a user