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

Merge pull request #6786 from netbox-community/nav-menu-plugins

Refactor navigation menu to support plugin items
This commit is contained in:
Jeremy Stretch
2021-07-23 08:00:13 -04:00
committed by GitHub
3 changed files with 265 additions and 233 deletions

View File

@ -1,23 +1,35 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Sequence, Optional 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[list] = None
color: Optional[str] = None
@dataclass @dataclass
class MenuItem: class MenuItem:
"""A navigation menu item link. Example: Sites, Platforms, RIRs, etc."""
label: str link: str
url: str link_text: str
disabled: bool = True permissions: Optional[list] = None
add_url: Optional[str] = None buttons: Optional[Sequence[MenuItemButton]] = None
import_url: Optional[str] = None
has_add: bool = False
has_import: bool = False
@dataclass @dataclass
class MenuGroup: class MenuGroup:
"""A group of menu items within a menu."""
label: str label: str
items: Sequence[MenuItem] items: Sequence[MenuItem]
@ -25,302 +37,338 @@ class MenuGroup:
@dataclass @dataclass
class Menu: class Menu:
"""A top level menu group. Example: Organization, Devices, IPAM."""
label: str label: str
icon: str icon_class: str
groups: Sequence[MenuGroup] 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
#
# Nav menus
#
ORGANIZATION_MENU = Menu( ORGANIZATION_MENU = Menu(
label="Organization", label='Organization',
icon="domain", icon_class='mdi mdi-domain',
groups=( groups=(
MenuGroup( MenuGroup(
label="Sites", label='Sites',
items=( items=(
MenuItem(label="Sites", url="dcim:site_list", get_model_item('dcim', 'site', 'Sites'),
add_url="dcim:site_add", import_url="dcim:site_import"), get_model_item('dcim', 'region', 'Regions'),
MenuItem(label="Site Groups", url="dcim:sitegroup_list", get_model_item('dcim', 'sitegroup', 'Site Groups'),
add_url="dcim:sitegroup_add", import_url="dcim:sitegroup_import"), get_model_item('dcim', 'location', 'Locations'),
MenuItem(label="Regions", url="dcim:region_list",
add_url="dcim:region_add", import_url="dcim:region_import"),
MenuItem(label="Locations", url="dcim:location_list",
add_url="dcim:location_add", import_url="dcim:location_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Racks", label='Racks',
items=( items=(
MenuItem(label="Racks", url="dcim:rack_list", get_model_item('dcim', 'rack', 'Racks'),
add_url="dcim:rack_add", import_url="dcim:rack_import"), get_model_item('dcim', 'rackrole', 'Rack Roles'),
MenuItem(label="Rack Roles", url="dcim:rackrole_list", get_model_item('dcim', 'rackreservation', 'Reservations'),
add_url="dcim:rackrole_add", import_url="dcim:rackrole_import"), MenuItem(
MenuItem(label="Reservations", url="dcim:rackreservation_list", link='dcim:rack_elevation_list',
add_url="dcim:rackreservation_add", import_url=None), link_text='Elevations',
MenuItem(label="Elevations", url="dcim:rack_elevation_list", permissions=['dcim.view_rack']
add_url=None, import_url=None), ),
), ),
), ),
MenuGroup( MenuGroup(
label="Tenancy", label='Tenancy',
items=( items=(
MenuItem(label="Tenants", url="tenancy:tenant_list", get_model_item('tenancy', 'tenant', 'Tenants'),
add_url="tenancy:tenant_add", import_url="tenancy:tenant_import"), get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
MenuItem(label="Tenant Groups",
url="tenancy:tenantgroup_list", add_url="tenancy:tenantgroup_add",
import_url="tenancy:tenantgroup_import"),
), ),
), ),
MenuGroup(
label="Tags",
items=(MenuItem(label="Tags", url="extras:tag_list",
add_url="extras:tag_add", import_url="extras:tag_import"),),
),
), ),
) )
DEVICES_MENU = Menu( DEVICES_MENU = Menu(
label="Devices", label='Devices',
icon="server", icon_class='mdi mdi-server',
groups=( groups=(
MenuGroup( MenuGroup(
label="Devices", label='Devices',
items=( items=(
MenuItem(label="Devices", url="dcim:device_list", get_model_item('dcim', 'device', 'Devices'),
add_url="dcim:device_add", import_url="dcim:device_import"), get_model_item('dcim', 'devicerole', 'Device Roles'),
MenuItem(label="Device Roles", url="dcim:devicerole_list", get_model_item('dcim', 'platform', 'Platforms'),
add_url="dcim:devicerole_add", import_url="dcim:devicerole_import"), get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),
MenuItem(label="Platforms", url="dcim:platform_list",
add_url="dcim:platform_add", import_url="dcim:platform_import"),
MenuItem(label="Virtual Chassis",
url="dcim:virtualchassis_list", add_url="dcim:virtualchassis_add",
import_url="dcim:virtualchassis_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Device Types", label='Device Types',
items=( items=(
MenuItem(label="Device Types", url="dcim:devicetype_list", get_model_item('dcim', 'devicetype', 'Device Types'),
add_url="dcim:devicetype_add", import_url="dcim:devicetype_import"), get_model_item('dcim', 'manufacturer', 'Manufacturers'),
MenuItem(label="Manufacturers", url="dcim:manufacturer_list",
add_url="dcim:manufacturer_add", import_url="dcim:manufacturer_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Connections", label='Device Components',
items=( items=(
MenuItem(label="Cables", url="dcim:cable_list", get_model_item('dcim', 'interface', 'Interfaces', actions=['import']),
add_url=None, import_url="dcim:cable_import"), get_model_item('dcim', 'frontport', 'Front Ports', actions=['import']),
get_model_item('dcim', 'rearport', 'Rear Ports', actions=['import']),
get_model_item('dcim', 'consoleport', 'Console Ports', actions=['import']),
get_model_item('dcim', 'consoleserverport', 'Console Server Ports', actions=['import']),
get_model_item('dcim', 'powerport', 'Power Ports', actions=['import']),
get_model_item('dcim', 'poweroutlet', 'Power Outlets', actions=['import']),
get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
),
),
),
)
CONNECTIONS_MENU = Menu(
label='Connections',
icon_class='mdi mdi-ethernet',
groups=(
MenuGroup(
label='Connections',
items=(
get_model_item('dcim', 'cable', 'Cables', actions=['import']),
MenuItem( MenuItem(
label="Console Connections", url="dcim:console_connections_list", add_url=None, import_url=None, link='dcim:interface_connections_list',
link_text='Interface Connections',
permissions=['dcim.view_interface']
), ),
MenuItem( MenuItem(
label="Interface Connections", url="dcim:interface_connections_list", add_url=None, import_url=None, link='dcim:console_connections_list',
link_text='Console Connections',
permissions=['dcim.view_consoleport']
),
MenuItem(
link='dcim:power_connections_list',
link_text='Power Connections',
permissions=['dcim.view_powerport']
), ),
MenuItem(label="Power Connections",
url="dcim:power_connections_list", add_url=None, import_url=None,),
),
),
MenuGroup(
label="Device Components",
items=(
MenuItem(label="Interfaces", url="dcim:interface_list",
add_url=None, import_url="dcim:interface_import"),
MenuItem(label="Front Ports", url="dcim:frontport_list",
add_url=None, import_url="dcim:frontport_import"),
MenuItem(label="Rear Ports", url="dcim:rearport_list",
add_url=None, import_url="dcim:rearport_import"),
MenuItem(label="Console Ports", url="dcim:consoleport_list",
add_url=None, import_url="dcim:consoleport_import"),
MenuItem(label="Console Server Ports", url="dcim:consoleserverport_list",
add_url=None, import_url="dcim:consoleserverport_import"),
MenuItem(label="Power Ports", url="dcim:powerport_list",
add_url=None, import_url="dcim:powerport_import"),
MenuItem(label="Power Outlets", url="dcim:poweroutlet_list",
add_url=None, import_url="dcim:poweroutlet_import"),
MenuItem(label="Device Bays", url="dcim:devicebay_list",
add_url=None, import_url="dcim:devicebay_import"),
MenuItem(label="Inventory Items",
url="dcim:inventoryitem_list", add_url=None, import_url="dcim:inventoryitem_import"),
), ),
), ),
), ),
) )
IPAM_MENU = Menu( IPAM_MENU = Menu(
label="IPAM", label='IPAM',
icon="counter", icon_class='mdi mdi-counter',
groups=( groups=(
MenuGroup( MenuGroup(
label="IP Addresses", label='IP Addresses',
items=( items=(
MenuItem(label="IP Ranges", url="ipam:iprange_list", get_model_item('ipam', 'ipaddress', 'IP Addresses'),
add_url="ipam:iprange_add", import_url="ipam:iprange_import"), get_model_item('ipam', 'iprange', 'IP Ranges'),
MenuItem(label="IP Addresses", url="ipam:ipaddress_list",
add_url="ipam:ipaddress_add", import_url="ipam:ipaddress_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Prefixes", label='Prefixes',
items=( items=(
MenuItem(label="Prefixes", url="ipam:prefix_list", get_model_item('ipam', 'prefix', 'Prefixes'),
add_url="ipam:prefix_add", import_url="ipam:prefix_import"), get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
MenuItem(label="Prefix & VLAN Roles", url="ipam:role_list",
add_url="ipam:role_add", import_url="ipam:role_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Aggregates", label='Aggregates',
items=( items=(
MenuItem(label="Aggregates", url="ipam:aggregate_list", get_model_item('ipam', 'aggregate', 'Aggregates'),
add_url="ipam:aggregate_add", import_url="ipam:aggregate_import"), get_model_item('ipam', 'rir', 'RIRs'),
MenuItem(label="RIRs", url="ipam:rir_list",
add_url="ipam:rir_add", import_url="ipam:rir_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="VRFs", label='VRFs',
items=( items=(
MenuItem(label="VRFs", url="ipam:vrf_list", get_model_item('ipam', 'vrf', 'VRFs'),
add_url="ipam:vrf_add", import_url="ipam:vrf_import"), get_model_item('ipam', 'routetarget', 'Route Targets'),
MenuItem(label="Route Targets", url="ipam:routetarget_list",
add_url="ipam:routetarget_add", import_url="ipam:routetarget_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="VLANs", label='VLANs',
items=( items=(
MenuItem(label="VLANs", url="ipam:vlan_list", get_model_item('ipam', 'vlan', 'VLANs'),
add_url="ipam:vlan_add", import_url="ipam:vlan_import"), get_model_item('ipam', 'vlangroup', 'VLAN Groups'),
MenuItem(label="VLAN Groups", url="ipam:vlangroup_list",
add_url="ipam:vlangroup_add", import_url="ipam:vlangroup_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Services", label='Services',
items=(MenuItem(label="Services", url="ipam:service_list", items=(
add_url=None, import_url="ipam:service_import"),), get_model_item('ipam', 'service', 'Services', actions=['import']),
),
), ),
), ),
) )
VIRTUALIZATION_MENU = Menu( VIRTUALIZATION_MENU = Menu(
label="Virtualization", label='Virtualization',
icon="monitor", icon_class='mdi mdi-monitor',
groups=( groups=(
MenuGroup( MenuGroup(
label="Virtual Machines", label='Virtual Machines',
items=( items=(
MenuItem( get_model_item('virtualization', 'virtualmachine', 'Virtual Machines'),
label="Virtual Machines", get_model_item('virtualization', 'vminterface', 'Interfaces', actions=['import']),
url="virtualization:virtualmachine_list", add_url="virtualization:virtualmachine_add", import_url="virtualization:virtualmachine_import"),
MenuItem(label="Interfaces",
url="virtualization:vminterface_list", add_url="virtualization:vminterface_add", import_url="virtualization:vminterface_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Clusters", label='Clusters',
items=( items=(
MenuItem(label="Clusters", url="virtualization:cluster_list", get_model_item('virtualization', 'cluster', 'Clusters'),
add_url="virtualization:cluster_add", import_url="virtualization:cluster_import"), get_model_item('virtualization', 'clustertype', 'Cluster Types'),
MenuItem(label="Cluster Types", get_model_item('virtualization', 'clustergroup', 'Cluster Groups'),
url="virtualization:clustertype_list", add_url="virtualization:clustertype_add", import_url="virtualization:clustertype_import"),
MenuItem(
label="Cluster Groups", url="virtualization:clustergroup_list", add_url="virtualization:clustergroup_add", import_url="virtualization:clustergroup_import"),
), ),
), ),
), ),
) )
CIRCUITS_MENU = Menu( CIRCUITS_MENU = Menu(
label="Circuits", label='Circuits',
icon="transit-connection-variant", icon_class='mdi mdi-transit-connection-variant',
groups=( groups=(
MenuGroup( MenuGroup(
label="Circuits", label='Circuits',
items=( items=(
MenuItem(label="Circuits", url="circuits:circuit_list", get_model_item('circuits', 'circuit', 'Circuits'),
add_url="circuits:circuit_add", import_url="circuits:circuit_import"), get_model_item('circuits', 'circuittype', 'Circuit Types'),
MenuItem(label="Circuit Types",
url="circuits:circuittype_list", add_url="circuits:circuittype_add", import_url="circuits:circuittype_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Providers", label='Providers',
items=( items=(
MenuItem(label="Providers", url="circuits:provider_list", get_model_item('circuits', 'provider', 'Providers'),
add_url="circuits:provider_add", import_url="circuits:provider_import"), get_model_item('circuits', 'providernetwork', 'Provider Networks'),
MenuItem(
label="Provider Networks", url="circuits:providernetwork_list", add_url="circuits:providernetwork_add", import_url="circuits:providernetwork_import"
),
), ),
), ),
), ),
) )
POWER_MENU = Menu( POWER_MENU = Menu(
label="Power", label='Power',
icon="flash", icon_class='mdi mdi-flash',
groups=( groups=(
MenuGroup( MenuGroup(
label="Power", label='Power',
items=( items=(
MenuItem(label="Power Feeds", url="dcim:powerfeed_list", get_model_item('dcim', 'powerfeed', 'Power Feeds'),
add_url="dcim:powerfeed_add", import_url="dcim:powerfeed_import"), get_model_item('dcim', 'powerpanel', 'Power Panels'),
MenuItem(label="Power Panels", url="dcim:powerpanel_list",
add_url="dcim:powerpanel_add", import_url="dcim:powerpanel_import"),
), ),
), ),
), ),
) )
OTHER_MENU = Menu( OTHER_MENU = Menu(
label="Other", label='Other',
icon="notification-clear-all", icon_class='mdi mdi-notification-clear-all',
groups=( groups=(
MenuGroup( MenuGroup(
label="Logging", label='Logging',
items=( items=(
MenuItem(label="Change Log", url="extras:objectchange_list", get_model_item('extras', 'journalentry', 'Journal Entries', actions=[]),
add_url=None, import_url=None), get_model_item('extras', 'objectchange', 'Change Log', actions=[]),
MenuItem(label="Journal Entries",
url="extras:journalentry_list", add_url=None, import_url=None),
MenuItem(label="Webhooks", url="extras:webhook_list",
add_url="extras:webhook_add", import_url="extras:webhook_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Customization", label='Customization',
items=( items=(
MenuItem(label="Custom Fields", url="extras:customfield_list", get_model_item('extras', 'customfield', 'Custom Fields'),
add_url="extras:customfield_add", import_url="extras:customfield_import"), get_model_item('extras', 'customlink', 'Custom Links'),
MenuItem(label="Custom Links", url="extras:customlink_list", get_model_item('extras', 'exporttemplate', 'Export Templates'),
add_url="extras:customlink_add", import_url="extras:customlink_import"),
MenuItem(label="Export Templates", url="extras:exporttemplate_list",
add_url="extras:exporttemplate_add", import_url="extras:exporttemplate_import"),
), ),
), ),
MenuGroup( MenuGroup(
label="Miscellaneous", label='Integrations',
items=( items=(
MenuItem(label="Config Contexts", url="extras:configcontext_list", get_model_item('extras', 'webhook', 'Webhooks'),
add_url="extras:configcontext_add", import_url=None), MenuItem(
MenuItem(label="Reports", url="extras:report_list", link='extras:report_list',
add_url=None, import_url=None), link_text='Reports',
MenuItem(label="Scripts", url="extras:script_list", permissions=['extras.view_report']
add_url=None, import_url=None), ),
MenuItem(
link='extras:script_list',
link_text='Scripts',
permissions=['extras.view_script']
),
),
),
MenuGroup(
label='Other',
items=(
get_model_item('extras', 'tag', 'Tags'),
get_model_item('extras', 'configcontext', 'Config Contexts', actions=['add']),
), ),
), ),
), ),
) )
MENUS = (
MENUS = [
ORGANIZATION_MENU, ORGANIZATION_MENU,
DEVICES_MENU, DEVICES_MENU,
CONNECTIONS_MENU,
IPAM_MENU, IPAM_MENU,
VIRTUALIZATION_MENU, VIRTUALIZATION_MENU,
CIRCUITS_MENU, CIRCUITS_MENU,
POWER_MENU, POWER_MENU,
OTHER_MENU, OTHER_MENU,
) ]
#
# Add plugin menus
#
if registry['plugin_menu_items']:
plugin_menu_groups = []
for plugin_name, items in registry['plugin_menu_items'].items():
plugin_menu_groups.append(
MenuGroup(
label=plugin_name,
items=items
)
)
PLUGIN_MENU = Menu(
label="Plugins",
icon_class="mdi mdi-puzzle",
groups=plugin_menu_groups
)
MENUS.append(PLUGIN_MENU)

View File

@ -1,3 +1,5 @@
{% load helpers %}
<div id="sidenav-accordion" class="accordion accordion-flush nav-item"> <div id="sidenav-accordion" class="accordion accordion-flush nav-item">
{% for menu in nav_items %} {% for menu in nav_items %}
@ -11,7 +13,7 @@
data-bs-target="#{{ menu.label|lower }}" data-bs-target="#{{ menu.label|lower }}"
class="d-flex justify-content-between align-items-center accordion-button nav-link collapsed"> class="d-flex justify-content-between align-items-center accordion-button nav-link collapsed">
<span class="fw-bold sidebar-nav-link"> <span class="fw-bold sidebar-nav-link">
<i class="mdi mdi-{{ menu.icon }} me-1 opacity-50"></i> <i class="{{ menu.icon_class }} me-1 opacity-50"></i>
{{ menu.label }} {{ menu.label }}
</span> </span>
</a> </a>
@ -23,33 +25,39 @@
{# Within each main menu, there are groups of menu items #} {# Within each main menu, there are groups of menu items #}
<div class="flex-column nav"> <div class="flex-column nav">
{% if menu.groups|length > 1 %} <h6 class="accordion-item-title">{{ group.label }}</h6>
<h6 class="accordion-item-title">{{ group.label }}</h6>
{% endif %}
{% for item in group.items %} {% for item in group.items %}
{# Each Menu Item #} {# Each Menu Item #}
<div class="nav-item d-flex justify-content-between align-items-center"> <div class="nav-item d-flex justify-content-between align-items-center">
{# Menu Link with Text #} {# Menu Link with Text #}
<a class="nav-link flex-grow-1" href="{% url item.url %}"> {% if request.user|has_perms:item.permissions %}
{{ item.label }}
</a> <a class="nav-link flex-grow-1" href="{% url item.link %}">
{{ item.link_text }}
</a>
{# Menu item buttons (if any) #}
{% if item.buttons %}
<div class="btn-group ps-1">
{% for button in item.buttons %}
{% if request.user|has_perms:button.permissions %}
<a class="btn btn-sm btn-{{ button.color }} lh-1" href="{% url button.link %}" title="{{ button.title }}">
<i class="{{ button.icon_class }}"></i>
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% else %}
{# Display a disabled link (no permission) #}
<a class="nav-link flex-grow-1 disabled">
{{ item.link_text }}
</a>
{# Add & Import Buttons #}
{% if item.has_add or item.has_import %}
<div class="btn-group ps-1">
{% if item.has_add %}
<a class="btn btn-sm btn-success lh-1" href="{% url item.add_url %}" title="Add {{ item.label }}">
<i class="mdi mdi-plus-thick"></i>
</a>
{% endif %}
{% if item.has_import %}
<a class="btn btn-sm btn-outline-success lh-1" href="{% url item.import_url %}" title="Import {{ item.label }}">
<i class="mdi mdi-upload"></i>
</a>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,43 +1,19 @@
from typing import Dict from typing import Dict
from django import template from django import template
from django.template import Context from django.template import Context
from django.contrib.auth.context_processors import PermWrapper
from netbox.navigation_menu import Menu, MenuGroup, MENUS from netbox.navigation_menu import MENUS
register = template.Library() register = template.Library()
def process_menu(menu: Menu, perms: PermWrapper) -> MenuGroup:
"""Enable a menu item if view permissions exist for the user."""
for group in menu.groups:
for item in group.items:
# Parse the URL template tag to a permission string.
app, scope = item.url.split(":")
object_name = scope.replace("_list", "")
view_perm = f"{app}.view_{scope}"
add_perm = f"{app}.add_object_name"
if view_perm in perms:
# If the view permission for each item exists, toggle
# the `disabled` field, which will be used in the UI.
item.disabled = False
if add_perm in perms:
if item.add_url is not None:
item.has_add = True
if item.import_url is not None:
item.has_import = True
return menu
@register.inclusion_tag("navigation/nav_items.html", takes_context=True) @register.inclusion_tag("navigation/nav_items.html", takes_context=True)
def nav(context: Context) -> Dict: def nav(context: Context) -> Dict:
"""Provide navigation items to template.""" """
perms: PermWrapper = context["perms"] Render the navigation menu.
groups = [process_menu(g, perms) for g in MENUS] """
return {
return {"nav_items": groups, "request": context["request"]} "nav_items": MENUS,
"request": context["request"]
}