1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #10441 from netbox-community/9071-plugin-menu

9071 add header to plugin menu
This commit is contained in:
Jeremy Stretch
2022-09-28 17:08:10 -04:00
committed by GitHub
8 changed files with 213 additions and 124 deletions

View File

@@ -6,15 +6,16 @@ from django.apps import AppConfig
from django.core.exceptions import ImproperlyConfigured
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.registry import registry
from netbox.navigation import MenuGroup
from utilities.choices import ButtonColorChoices
# Initialize plugin registry
registry['plugins'] = {
'graphql_schemas': [],
'menus': [],
'menu_items': {},
'preferences': {},
'template_extensions': collections.defaultdict(list),
@@ -57,6 +58,7 @@ class PluginConfig(AppConfig):
# Default integration paths. Plugin authors can override these to customize the paths to
# integrated components.
graphql_schema = 'graphql.schema'
menu = 'navigation.menu'
menu_items = 'navigation.menu_items'
template_extensions = 'template_content.template_extensions'
user_preferences = 'preferences.preferences'
@@ -69,9 +71,10 @@ class PluginConfig(AppConfig):
if template_extensions is not None:
register_template_extensions(template_extensions)
# Register navigation menu items (if defined)
menu_items = import_object(f"{self.__module__}.{self.menu_items}")
if menu_items is not None:
# Register navigation menu or menu items (if defined)
if menu := import_object(f"{self.__module__}.{self.menu}"):
register_menu(menu)
if menu_items := import_object(f"{self.__module__}.{self.menu_items}"):
register_menu_items(self.verbose_name, menu_items)
# Register GraphQL schema (if defined)
@@ -200,6 +203,18 @@ def register_template_extensions(class_list):
# Navigation menu links
#
class PluginMenu:
icon_class = 'mdi mdi-puzzle'
def __init__(self, label, groups, icon_class=None):
self.label = label
self.groups = [
MenuGroup(label, items) for label, items in groups
]
if icon_class is not None:
self.icon_class = icon_class
class PluginMenuItem:
"""
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
@@ -246,6 +261,12 @@ class PluginMenuButton:
self.color = color
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)

View File

@@ -1,7 +1,7 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
menu_items = (
items = (
PluginMenuItem(
link='plugins:dummy_plugin:dummy_models',
link_text='Item 1',
@@ -23,3 +23,9 @@ menu_items = (
link_text='Item 2',
),
)
menu = PluginMenu(
label='Dummy',
groups=(('Group 1', items),),
)
menu_items = items

View File

@@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from extras.plugins import PluginMenu
from extras.registry import registry
from extras.tests.dummy_plugin import config as dummy_config
from netbox.graphql.schema import Query
@@ -58,9 +59,17 @@ class PluginTest(TestCase):
response = client.get(url)
self.assertEqual(response.status_code, 200)
def test_menu(self):
"""
Check menu registration.
"""
menu = registry['plugins']['menus'][0]
self.assertIsInstance(menu, PluginMenu)
self.assertEqual(menu.label, 'Dummy')
def test_menu_items(self):
"""
Check that plugin MenuItems and MenuButtons are registered.
Check menu_items registration.
"""
self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
menu_items = registry['plugins']['menu_items']['Dummy plugin']

View File

@@ -0,0 +1,92 @@
from dataclasses import dataclass
from typing import Sequence, Optional
from utilities.choices import ButtonColorChoices
__all__ = (
'get_model_item',
'get_model_buttons',
'Menu',
'MenuGroup',
'MenuItem',
'MenuItemButton',
)
#
# Navigation menu data classes
#
@dataclass
class MenuItemButton:
link: str
title: str
icon_class: str
permissions: Optional[Sequence[str]] = ()
color: Optional[str] = None
@dataclass
class MenuItem:
link: str
link_text: str
permissions: Optional[Sequence[str]] = ()
buttons: Optional[Sequence[MenuItemButton]] = ()
@dataclass
class MenuGroup:
label: str
items: Sequence[MenuItem]
@dataclass
class Menu:
label: str
icon_class: str
groups: Sequence[MenuGroup]
#
# Utility functions
#
def get_model_item(app_label, model_name, label, actions=('add', 'import')):
return MenuItem(
link=f'{app_label}:{model_name}_list',
link_text=label,
permissions=[f'{app_label}.view_{model_name}'],
buttons=get_model_buttons(app_label, model_name, actions)
)
def get_model_buttons(app_label, model_name, actions=('add', 'import')):
buttons = []
if 'add' in actions:
buttons.append(
MenuItemButton(
link=f'{app_label}:{model_name}_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.GREEN
)
)
if 'import' in actions:
buttons.append(
MenuItemButton(
link=f'{app_label}:{model_name}_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.CYAN
)
)
return buttons

View File

@@ -1,86 +1,5 @@
from dataclasses import dataclass
from typing import Sequence, Optional
from extras.registry import registry
from utilities.choices import ButtonColorChoices
#
# Nav menu data classes
#
@dataclass
class MenuItemButton:
link: str
title: str
icon_class: str
permissions: Optional[Sequence[str]] = ()
color: Optional[str] = None
@dataclass
class MenuItem:
link: str
link_text: str
permissions: Optional[Sequence[str]] = ()
buttons: Optional[Sequence[MenuItemButton]] = ()
@dataclass
class MenuGroup:
label: str
items: Sequence[MenuItem]
@dataclass
class Menu:
label: str
icon_class: str
groups: Sequence[MenuGroup]
#
# Utility functions
#
def get_model_item(app_label, model_name, label, actions=('add', 'import')):
return MenuItem(
link=f'{app_label}:{model_name}_list',
link_text=label,
permissions=[f'{app_label}.view_{model_name}'],
buttons=get_model_buttons(app_label, model_name, actions)
)
def get_model_buttons(app_label, model_name, actions=('add', 'import')):
buttons = []
if 'add' in actions:
buttons.append(
MenuItemButton(
link=f'{app_label}:{model_name}_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.GREEN
)
)
if 'import' in actions:
buttons.append(
MenuItemButton(
link=f'{app_label}:{model_name}_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.CYAN
)
)
return buttons
from . import *
#
@@ -405,21 +324,19 @@ MENUS = [
# Add plugin menus
#
for menu in registry['plugins']['menus']:
MENUS.append(menu)
if registry['plugins']['menu_items']:
plugin_menu_groups = []
for plugin_name, items in registry['plugins']['menu_items'].items():
plugin_menu_groups.append(
MenuGroup(
label=plugin_name,
items=items
)
)
PLUGIN_MENU = Menu(
# Build the default plugins menu
groups = [
MenuGroup(label=label, items=items)
for label, items in registry['plugins']['menu_items'].items()
]
plugins_menu = Menu(
label="Plugins",
icon_class="mdi mdi-puzzle",
groups=plugin_menu_groups
groups=groups
)
MENUS.append(PLUGIN_MENU)
MENUS.append(plugins_menu)

View File

@@ -2,7 +2,7 @@ from typing import Dict
from django import template
from django.template import Context
from netbox.navigation_menu import MENUS
from netbox.navigation.menu import MENUS
register = template.Library()