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 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
class MenuItem:
"""A navigation menu item link. Example: Sites, Platforms, RIRs, etc."""
label: str
url: str
disabled: bool = True
add_url: Optional[str] = None
import_url: Optional[str] = None
has_add: bool = False
has_import: bool = False
link: str
link_text: str
permissions: Optional[list] = None
buttons: Optional[Sequence[MenuItemButton]] = None
@dataclass
class MenuGroup:
"""A group of menu items within a menu."""
label: str
items: Sequence[MenuItem]
@ -25,302 +37,338 @@ class MenuGroup:
@dataclass
class Menu:
"""A top level menu group. Example: Organization, Devices, IPAM."""
label: str
icon: 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
#
# Nav menus
#
ORGANIZATION_MENU = Menu(
label="Organization",
icon="domain",
label='Organization',
icon_class='mdi mdi-domain',
groups=(
MenuGroup(
label="Sites",
label='Sites',
items=(
MenuItem(label="Sites", url="dcim:site_list",
add_url="dcim:site_add", import_url="dcim:site_import"),
MenuItem(label="Site Groups", url="dcim:sitegroup_list",
add_url="dcim:sitegroup_add", import_url="dcim:sitegroup_import"),
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"),
get_model_item('dcim', 'site', 'Sites'),
get_model_item('dcim', 'region', 'Regions'),
get_model_item('dcim', 'sitegroup', 'Site Groups'),
get_model_item('dcim', 'location', 'Locations'),
),
),
MenuGroup(
label="Racks",
label='Racks',
items=(
MenuItem(label="Racks", url="dcim:rack_list",
add_url="dcim:rack_add", import_url="dcim:rack_import"),
MenuItem(label="Rack Roles", url="dcim:rackrole_list",
add_url="dcim:rackrole_add", import_url="dcim:rackrole_import"),
MenuItem(label="Reservations", url="dcim:rackreservation_list",
add_url="dcim:rackreservation_add", import_url=None),
MenuItem(label="Elevations", url="dcim:rack_elevation_list",
add_url=None, import_url=None),
get_model_item('dcim', 'rack', 'Racks'),
get_model_item('dcim', 'rackrole', 'Rack Roles'),
get_model_item('dcim', 'rackreservation', 'Reservations'),
MenuItem(
link='dcim:rack_elevation_list',
link_text='Elevations',
permissions=['dcim.view_rack']
),
),
),
MenuGroup(
label="Tenancy",
label='Tenancy',
items=(
MenuItem(label="Tenants", url="tenancy:tenant_list",
add_url="tenancy:tenant_add", import_url="tenancy:tenant_import"),
MenuItem(label="Tenant Groups",
url="tenancy:tenantgroup_list", add_url="tenancy:tenantgroup_add",
import_url="tenancy:tenantgroup_import"),
get_model_item('tenancy', 'tenant', 'Tenants'),
get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
),
),
MenuGroup(
label="Tags",
items=(MenuItem(label="Tags", url="extras:tag_list",
add_url="extras:tag_add", import_url="extras:tag_import"),),
),
),
)
DEVICES_MENU = Menu(
label="Devices",
icon="server",
label='Devices',
icon_class='mdi mdi-server',
groups=(
MenuGroup(
label="Devices",
label='Devices',
items=(
MenuItem(label="Devices", url="dcim:device_list",
add_url="dcim:device_add", import_url="dcim:device_import"),
MenuItem(label="Device Roles", url="dcim:devicerole_list",
add_url="dcim:devicerole_add", import_url="dcim:devicerole_import"),
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"),
get_model_item('dcim', 'device', 'Devices'),
get_model_item('dcim', 'devicerole', 'Device Roles'),
get_model_item('dcim', 'platform', 'Platforms'),
get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),
),
),
MenuGroup(
label="Device Types",
label='Device Types',
items=(
MenuItem(label="Device Types", url="dcim:devicetype_list",
add_url="dcim:devicetype_add", import_url="dcim:devicetype_import"),
MenuItem(label="Manufacturers", url="dcim:manufacturer_list",
add_url="dcim:manufacturer_add", import_url="dcim:manufacturer_import"),
get_model_item('dcim', 'devicetype', 'Device Types'),
get_model_item('dcim', 'manufacturer', 'Manufacturers'),
),
),
MenuGroup(
label="Connections",
label='Device Components',
items=(
MenuItem(label="Cables", url="dcim:cable_list",
add_url=None, import_url="dcim:cable_import"),
get_model_item('dcim', 'interface', 'Interfaces', actions=['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(
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(
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(
label="IPAM",
icon="counter",
label='IPAM',
icon_class='mdi mdi-counter',
groups=(
MenuGroup(
label="IP Addresses",
label='IP Addresses',
items=(
MenuItem(label="IP Ranges", url="ipam:iprange_list",
add_url="ipam:iprange_add", import_url="ipam:iprange_import"),
MenuItem(label="IP Addresses", url="ipam:ipaddress_list",
add_url="ipam:ipaddress_add", import_url="ipam:ipaddress_import"),
get_model_item('ipam', 'ipaddress', 'IP Addresses'),
get_model_item('ipam', 'iprange', 'IP Ranges'),
),
),
MenuGroup(
label="Prefixes",
label='Prefixes',
items=(
MenuItem(label="Prefixes", url="ipam:prefix_list",
add_url="ipam:prefix_add", import_url="ipam:prefix_import"),
MenuItem(label="Prefix & VLAN Roles", url="ipam:role_list",
add_url="ipam:role_add", import_url="ipam:role_import"),
get_model_item('ipam', 'prefix', 'Prefixes'),
get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
),
),
MenuGroup(
label="Aggregates",
label='Aggregates',
items=(
MenuItem(label="Aggregates", url="ipam:aggregate_list",
add_url="ipam:aggregate_add", import_url="ipam:aggregate_import"),
MenuItem(label="RIRs", url="ipam:rir_list",
add_url="ipam:rir_add", import_url="ipam:rir_import"),
get_model_item('ipam', 'aggregate', 'Aggregates'),
get_model_item('ipam', 'rir', 'RIRs'),
),
),
MenuGroup(
label="VRFs",
label='VRFs',
items=(
MenuItem(label="VRFs", url="ipam:vrf_list",
add_url="ipam:vrf_add", import_url="ipam:vrf_import"),
MenuItem(label="Route Targets", url="ipam:routetarget_list",
add_url="ipam:routetarget_add", import_url="ipam:routetarget_import"),
get_model_item('ipam', 'vrf', 'VRFs'),
get_model_item('ipam', 'routetarget', 'Route Targets'),
),
),
MenuGroup(
label="VLANs",
label='VLANs',
items=(
MenuItem(label="VLANs", url="ipam:vlan_list",
add_url="ipam:vlan_add", import_url="ipam:vlan_import"),
MenuItem(label="VLAN Groups", url="ipam:vlangroup_list",
add_url="ipam:vlangroup_add", import_url="ipam:vlangroup_import"),
get_model_item('ipam', 'vlan', 'VLANs'),
get_model_item('ipam', 'vlangroup', 'VLAN Groups'),
),
),
MenuGroup(
label="Services",
items=(MenuItem(label="Services", url="ipam:service_list",
add_url=None, import_url="ipam:service_import"),),
label='Services',
items=(
get_model_item('ipam', 'service', 'Services', actions=['import']),
),
),
),
)
VIRTUALIZATION_MENU = Menu(
label="Virtualization",
icon="monitor",
label='Virtualization',
icon_class='mdi mdi-monitor',
groups=(
MenuGroup(
label="Virtual Machines",
label='Virtual Machines',
items=(
MenuItem(
label="Virtual Machines",
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"),
get_model_item('virtualization', 'virtualmachine', 'Virtual Machines'),
get_model_item('virtualization', 'vminterface', 'Interfaces', actions=['import']),
),
),
MenuGroup(
label="Clusters",
label='Clusters',
items=(
MenuItem(label="Clusters", url="virtualization:cluster_list",
add_url="virtualization:cluster_add", import_url="virtualization:cluster_import"),
MenuItem(label="Cluster Types",
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"),
get_model_item('virtualization', 'cluster', 'Clusters'),
get_model_item('virtualization', 'clustertype', 'Cluster Types'),
get_model_item('virtualization', 'clustergroup', 'Cluster Groups'),
),
),
),
)
CIRCUITS_MENU = Menu(
label="Circuits",
icon="transit-connection-variant",
label='Circuits',
icon_class='mdi mdi-transit-connection-variant',
groups=(
MenuGroup(
label="Circuits",
label='Circuits',
items=(
MenuItem(label="Circuits", url="circuits:circuit_list",
add_url="circuits:circuit_add", import_url="circuits:circuit_import"),
MenuItem(label="Circuit Types",
url="circuits:circuittype_list", add_url="circuits:circuittype_add", import_url="circuits:circuittype_import"),
get_model_item('circuits', 'circuit', 'Circuits'),
get_model_item('circuits', 'circuittype', 'Circuit Types'),
),
),
MenuGroup(
label="Providers",
label='Providers',
items=(
MenuItem(label="Providers", url="circuits:provider_list",
add_url="circuits:provider_add", import_url="circuits:provider_import"),
MenuItem(
label="Provider Networks", url="circuits:providernetwork_list", add_url="circuits:providernetwork_add", import_url="circuits:providernetwork_import"
),
get_model_item('circuits', 'provider', 'Providers'),
get_model_item('circuits', 'providernetwork', 'Provider Networks'),
),
),
),
)
POWER_MENU = Menu(
label="Power",
icon="flash",
label='Power',
icon_class='mdi mdi-flash',
groups=(
MenuGroup(
label="Power",
label='Power',
items=(
MenuItem(label="Power Feeds", url="dcim:powerfeed_list",
add_url="dcim:powerfeed_add", import_url="dcim:powerfeed_import"),
MenuItem(label="Power Panels", url="dcim:powerpanel_list",
add_url="dcim:powerpanel_add", import_url="dcim:powerpanel_import"),
get_model_item('dcim', 'powerfeed', 'Power Feeds'),
get_model_item('dcim', 'powerpanel', 'Power Panels'),
),
),
),
)
OTHER_MENU = Menu(
label="Other",
icon="notification-clear-all",
label='Other',
icon_class='mdi mdi-notification-clear-all',
groups=(
MenuGroup(
label="Logging",
label='Logging',
items=(
MenuItem(label="Change Log", url="extras:objectchange_list",
add_url=None, import_url=None),
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"),
get_model_item('extras', 'journalentry', 'Journal Entries', actions=[]),
get_model_item('extras', 'objectchange', 'Change Log', actions=[]),
),
),
MenuGroup(
label="Customization",
label='Customization',
items=(
MenuItem(label="Custom Fields", url="extras:customfield_list",
add_url="extras:customfield_add", import_url="extras:customfield_import"),
MenuItem(label="Custom Links", url="extras:customlink_list",
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"),
get_model_item('extras', 'customfield', 'Custom Fields'),
get_model_item('extras', 'customlink', 'Custom Links'),
get_model_item('extras', 'exporttemplate', 'Export Templates'),
),
),
MenuGroup(
label="Miscellaneous",
label='Integrations',
items=(
MenuItem(label="Config Contexts", url="extras:configcontext_list",
add_url="extras:configcontext_add", import_url=None),
MenuItem(label="Reports", url="extras:report_list",
add_url=None, import_url=None),
MenuItem(label="Scripts", url="extras:script_list",
add_url=None, import_url=None),
get_model_item('extras', 'webhook', 'Webhooks'),
MenuItem(
link='extras:report_list',
link_text='Reports',
permissions=['extras.view_report']
),
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,
DEVICES_MENU,
CONNECTIONS_MENU,
IPAM_MENU,
VIRTUALIZATION_MENU,
CIRCUITS_MENU,
POWER_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">
{% for menu in nav_items %}
@ -11,7 +13,7 @@
data-bs-target="#{{ menu.label|lower }}"
class="d-flex justify-content-between align-items-center accordion-button nav-link collapsed">
<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 }}
</span>
</a>
@ -23,33 +25,39 @@
{# Within each main menu, there are groups of menu items #}
<div class="flex-column nav">
{% if menu.groups|length > 1 %}
<h6 class="accordion-item-title">{{ group.label }}</h6>
{% endif %}
<h6 class="accordion-item-title">{{ group.label }}</h6>
{% for item in group.items %}
{# Each Menu Item #}
<div class="nav-item d-flex justify-content-between align-items-center">
{# Menu Link with Text #}
<a class="nav-link flex-grow-1" href="{% url item.url %}">
{{ item.label }}
</a>
{% if request.user|has_perms:item.permissions %}
<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 %}
</div>

View File

@ -1,43 +1,19 @@
from typing import Dict
from django import template
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()
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)
def nav(context: Context) -> Dict:
"""Provide navigation items to template."""
perms: PermWrapper = context["perms"]
groups = [process_menu(g, perms) for g in MENUS]
return {"nav_items": groups, "request": context["request"]}
"""
Render the navigation menu.
"""
return {
"nav_items": MENUS,
"request": context["request"]
}