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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
92
netbox/netbox/navigation/__init__.py
Normal file
92
netbox/netbox/navigation/__init__.py
Normal 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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user