Răsfoiți Sursa

Merge branch 'jstretch-ui-work' into feature

jeremystretch 4 ani în urmă
părinte
comite
5406acf329

+ 322 - 0
netbox/netbox/navigation_menu.py

@@ -0,0 +1,322 @@
+from dataclasses import dataclass
+from typing import Sequence, Optional
+
+
+@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
+
+
+@dataclass
+class MenuGroup:
+    """A group of menu items within a menu."""
+
+    label: str
+    items: Sequence[MenuItem]
+
+
+@dataclass
+class Menu:
+    """A top level menu group. Example: Organization, Devices, IPAM."""
+
+    label: str
+    icon: str
+    groups: Sequence[MenuGroup]
+
+
+ORGANIZATION_MENU = Menu(
+    label="Organization",
+    icon="domain",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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="Elevations", url="dcim:rack_elevation_list",
+                         add_url=None, import_url=None),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        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",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            label="Connections",
+            items=(
+                MenuItem(label="Cables", url="dcim:cable_list",
+                         add_url=None, import_url="dcim:cable_import"),
+                MenuItem(
+                    label="Console Connections", url="dcim:console_connections_list", add_url=None, import_url=None,
+                ),
+                MenuItem(
+                    label="Interface Connections", url="dcim:interface_connections_list", add_url=None, import_url=None,
+                ),
+                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",
+    groups=(
+        MenuGroup(
+            label="IP Addresses",
+            items=(
+                MenuItem(label="IP Addresses", url="ipam:ipaddress_list",
+                         add_url="ipam:ipaddress_add", import_url="ipam:ipaddress_import"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            label="Services",
+            items=(MenuItem(label="Services", url="ipam:service_list",
+                   add_url=None, import_url="ipam:service_import"),),
+        ),
+    ),
+)
+
+VIRTUALIZATION_MENU = Menu(
+    label="Virtualization",
+    icon="monitor",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+    ),
+)
+
+CIRCUITS_MENU = Menu(
+    label="Circuits",
+    icon="transit-connection-variant",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"
+                ),
+            ),
+        ),
+    ),
+)
+
+POWER_MENU = Menu(
+    label="Power",
+    icon="flash",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+    ),
+)
+
+OTHER_MENU = Menu(
+    label="Other",
+    icon="notification-clear-all",
+    groups=(
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            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"),
+            ),
+        ),
+        MenuGroup(
+            label="Miscellaneous",
+            items=(
+                MenuItem(label="Config Contexts",
+                         url="extras:configcontext_list", add_url=None, 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),
+            ),
+        ),
+    ),
+)
+
+MENUS = (
+    ORGANIZATION_MENU,
+    DEVICES_MENU,
+    IPAM_MENU,
+    VIRTUALIZATION_MENU,
+    CIRCUITS_MENU,
+    POWER_MENU,
+    OTHER_MENU,
+)

+ 16 - 10
netbox/templates/base/layout.html

@@ -6,35 +6,41 @@
 {% load static %}
 
 {% block layout %}
-  <div class="container-fluid">
-    <main class="ms-sm-auto px-0">
+  <div class="container-fluid px-0">
+    <main class="ms-sm-auto">
     {# Sidebar #}
       <nav id="sidebar-menu" class="d-md-block sidebar collapse px-0" data-simplebar>
 
         {# Sidebar content #}
-        <div class="position-sticky pt-3">
+        <div class="position-sticky">
 
           {# Logo #}
-          <a class="p-1 sidebar-logo d-none d-md-flex justify-content-center" href="{% url 'home' %}">
-            <img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" />
-          </a>
+          <div class="py-2">
+            <a class="sidebar-logo d-none d-md-flex justify-content-center" href="{% url 'home' %}">
+              <img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" />
+            </a>
+          </div>
+
+          <ul class="nav flex-column px-1">
 
-          {# Search bar #}
-          <ul class="nav flex-column px-4">
+            {# Search bar for collapsed menu #}
             <div class="d-block d-md-none mx-1 my-3 search-container">
               {% search_options %}
             </div>
             <div class="d-flex d-md-none mx-1 my-3 justify-content-center justify-content-md-end order-last order-md-0">
               {% include 'inc/profile_button.html' %}
             </div>
+
+            {# Navigation menu #}
             {% nav %}
+
           </ul>
 
         </div>
 
         {# Sidebar footer #}
         <div class="d-flex flex-column container-fluid mt-auto justify-content-end sidebar-bottom">
-          <nav class="nav justify-content-between mb-2 mt-4 px-2">
+          <nav class="nav justify-content-evenly my-2 px-2">
 
             {# Documentation #}
             <a type="button" class="nav-link" href="https://netbox.readthedocs.io/" target="_blank">
@@ -89,7 +95,7 @@
 
         {# Page header #}
         {% block header %}
-          <div class="title-container px-3 py-3">
+          <div class="title-container px-3 pb-3">
 
             {# Title #}
             <div id="content-title">

+ 19 - 29
netbox/templates/generic/object_bulk_delete.html

@@ -4,39 +4,29 @@
 {% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 
 {% block content %}
-    <div class="row">
-        <div class="col col-md-12 col-lg-8 offset-lg-2">
-            <div class="alert alert-danger mb-3" role="alert">
-                <h4 class="alert-heading">Confirm Bulk Deletion</h4>
-                <hr />
-                <div>
-                    <strong>Warning:</strong> The following operation will delete <strong>{{ table.rows|length }}</strong> {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.
-                </div>
-                {% block message_extra %}{% endblock %}
+    <div class="container-md px-0">
+        <div class="alert alert-danger mb-3" role="alert">
+            <h4 class="alert-heading">Confirm Bulk Deletion</h4>
+            <hr />
+            <div>
+                <strong>Warning:</strong> The following operation will delete <strong>{{ table.rows|length }}</strong> {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.
             </div>
+            {% block message_extra %}{% endblock %}
         </div>
     </div>
-    <div class="row">
-        <div class="col col-md-12 col-lg-8 offset-lg-2">
-            <div class="card">
-                <div class="card-body">
-                    {% include 'inc/table.html' %}
-                </div>
+    <div class="container-xl px-0">
+      {% include 'inc/table.html' %}
+      <div class="row mt-3">
+        <form action="" method="post">
+            {% csrf_token %}
+            {% for field in form.hidden_fields %}
+                {{ field }}
+            {% endfor %}
+            <div class="text-end">
+                <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
+                <button type="submit" name="_confirm" class="btn btn-danger">Delete {{ table.rows|length }} {{ obj_type_plural }}</button>
             </div>
-        </div>
+        </form>
     </div>
-    <div class="row mt-3">
-        <div class="col col-md-12 col-lg-8 offset-lg-2">
-            <form action="" method="post">
-                {% csrf_token %}
-                {% for field in form.hidden_fields %}
-                    {{ field }}
-                {% endfor %}
-                <div class="text-end">
-                    <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
-                    <button type="submit" name="_confirm" class="btn btn-danger">Delete {{ table.rows|length }} {{ obj_type_plural }}</button>
-                </div>
-            </form>
-        </div>
     </div>
 {% endblock content %}

+ 1 - 5
netbox/templates/generic/object_bulk_edit.html

@@ -15,11 +15,7 @@
     {% endfor %}
     <div class="row mb-3">
         <div class="col col-md-8">
-            <div class="card">
-                <div class="card-body">
-                    {% include 'inc/table.html' %}
-                </div>
-            </div>
+          {% include 'inc/table.html' %}
         </div>
         <div class="col col-md-4">
             <div class="card">

+ 18 - 30
netbox/templates/generic/object_bulk_remove.html

@@ -4,37 +4,25 @@
 {% block title %}Remove {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 
 {% block content %}
-<div class="row mb-3">
-    <div class="col col-md-6 offset-md-3">
-        <div class="alert alert-danger" role="alert">
-            <h4 class="alert-heading">Confirm Bulk Removal</h4>
-            <p><strong>Warning:</strong> The following operation will remove {{ table.rows|length }} {{ obj_type_plural }} from {{ parent_obj }}.</p>
-            <hr />
-            <p class="mb-0">Please carefully review the {{ obj_type_plural }} to be removed and confirm below.</p>
-        </div>
-    </div>
-</div>
-<div class="row mb-3">
-    <div class="col col-md-12">
-        <div class="card">
-            <div class="card-body">
-                {% include 'inc/table.html' %}
-            </div>
-        </div>
-    </div>
+<div class="container-md px-0">
+  <div class="alert alert-danger" role="alert">
+    <h4 class="alert-heading">Confirm Bulk Removal</h4>
+    <p><strong>Warning:</strong> The following operation will remove {{ table.rows|length }} {{ obj_type_plural }} from {{ parent_obj }}.</p>
+    <hr />
+    <p class="mb-0">Please carefully review the {{ obj_type_plural }} to be removed and confirm below.</p>
+  </div>
 </div>
-<div class="row mb-3">
-    <div class="col col-md-6 offset-md-3">
-        <form action="." method="post" class="form">
-            {% csrf_token %}
-            {% for field in form.hidden_fields %}
-                {{ field }}
-            {% endfor %}
-            <div class="text-center">
-                <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
-                <button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
-            </div>
-        </form>
+<div class="container-xl px-0">
+  {% include 'inc/table.html' %}
+  <form action="." method="post" class="form">
+    {% csrf_token %}
+    {% for field in form.hidden_fields %}
+      {{ field }}
+    {% endfor %}
+    <div class="text-center">
+      <a href="{{ return_url }}" class="btn btn-outline-dark">Cancel</a>
+      <button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
     </div>
+  </form>
 </div>
 {% endblock content %}

+ 16 - 20
netbox/templates/generic/object_bulk_rename.html

@@ -7,26 +7,22 @@
 {% block content %}
 <div class="row mb-3">
     <div class="col col-md-7">
-        <div class="card">
-            <div class="card-body">
-                <table class="table">
-                    <thead>
-                        <tr>
-                            <th>Current Name</th>
-                            <th>New Name</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        {% for obj in selected_objects %}
-                            <tr{% if obj.new_name and obj.name != obj.new_name %} class="success"{% endif %}>
-                                <td>{{ obj.name }}</td>
-                                <td>{{ obj.new_name }}</td>
-                            </tr>
-                        {% endfor %}
-                    </tbody>
-                </table>
-            </div>
-        </div>
+        <table class="table">
+            <thead>
+                <tr>
+                    <th>Current Name</th>
+                    <th>New Name</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for obj in selected_objects %}
+                    <tr{% if obj.new_name and obj.name != obj.new_name %} class="success"{% endif %}>
+                        <td>{{ obj.name }}</td>
+                        <td>{{ obj.new_name }}</td>
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
     </div>
     <div class="col col-md-5">
         <form action="" method="post" class="form form-horizontal">

+ 81 - 83
netbox/templates/generic/object_list.html

@@ -25,98 +25,96 @@
 
 {% block content %}
 {% if filter_form %}
-    <div class="col col-md-12 noprint">
-        {% include 'inc/advanced_search.html' %}
-    </div>
+  {% include 'inc/advanced_search.html' %}
 {% endif %}
 {% if table.paginator.num_pages > 1 %}
 {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
-<div class="row mb-3">
-    <form method="post" class="form col-md-12">
+  <div id="select-all-box" class="d-none card noprint">
+    <div class="row mb-3">
+      <form method="post" class="form col-md-12">
         {% csrf_token %}
-        <div id="select-all-box" class="d-none card noprint">
-            <div class="card-body d-inline-flex justify-content-between align-items-center">
-                <div class="form-check">
-                    <input type="checkbox" id="select-all" name="_all" class="form-check-input" />
-                    <label for="select-all" class="form-check-label">
-                        Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> Matching Query
-                    </label>
-                </div>
-                <div class="float-end">
-                    {% if bulk_edit_url and permissions.change %}
-                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
-                            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
-                        </button>
-                    {% endif %}
-                    {% if bulk_delete_url and permissions.delete %}
-                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
-                            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
-                        </button>
-                    {% endif %}
-                </div>
-            </div>
+        <div class="card-body d-inline-flex justify-content-between align-items-center">
+          <div class="form-check">
+            <input type="checkbox" id="select-all" name="_all" class="form-check-input" />
+            <label for="select-all" class="form-check-label">
+              Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> Matching Query
+            </label>
+          </div>
+          <div class="float-end">
+            {% if bulk_edit_url and permissions.change %}
+              <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
+                <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
+              </button>
+            {% endif %}
+            {% if bulk_delete_url and permissions.delete %}
+              <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
+                <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
+              </button>
+            {% endif %}
+          </div>
         </div>
-    </form>
-</div>
+      </form>
+    </div>
+  </div>
 {% endwith %}
 {% endif %}
+
+{# Object list filter, table config #}
 <div class="row mb-3">
-    <div class="col col-md-12">
-        <div class="card">
-            <div class="card-header">
-                <div class="row">
-                    <div class="col col-md-4 offset-md-8 d-flex noprint table-controls">
-                        <div class="input-group input-group-sm">
-                            <input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
-                            {% if request.user.is_authenticated and table_config_form %}
-                                <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#ObjectTable_config" title="Configure Table">
-                                    <i class="mdi mdi-table-eye"></i>
-                                </button>
-                            {% endif %}
-                            {% if filter_form %}
-                            <button
-                                type="button"
-                                class="btn btn-sm btn-outline-dark"
-                                data-bs-toggle="collapse"
-                                data-bs-target="#advanced-search-content">
-                                Advanced Search
-                            </button>
-                        {% endif %}
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="card-body">
-                {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
-                {% if permissions.change or permissions.delete %}
-                <form method="post" class="form form-horizontal">
-                    {% csrf_token %}
-                    <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
-                    {% include table_template|default:'inc/responsive_table.html' %}
-                    <div class="float-start noprint bulk-buttons">
-                        {% block bulk_buttons %}{% endblock %}
-                        {% if bulk_edit_url and permissions.change %}
-                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
-                            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
-                        </button>
-                        {% endif %}
-                        {% if bulk_delete_url and permissions.delete %}
-                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
-                            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
-                        </button>
-                        {% endif %}
-                    </div>
-                </form>
-                {% else %}
-                    <div class="table-responsive">
-                        {% render_table table 'inc/table.html' %}
-                    </div>
-                {% endif %}
-                {% endwith %}
-                {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
-            </div>
-        </div>
+  <div class="col col-md-4 offset-md-8 d-flex noprint table-controls">
+    <div class="input-group input-group-sm">
+      <input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
+      {% if request.user.is_authenticated and table_config_form %}
+        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#ObjectTable_config" title="Configure Table">
+          <i class="mdi mdi-table-eye"></i>
+        </button>
+      {% endif %}
+      {% if filter_form %}
+      <button
+        type="button"
+        class="btn btn-sm btn-outline-dark"
+        data-bs-toggle="collapse"
+        data-bs-target="#advanced-search-content">
+        Advanced Search
+      </button>
+    {% endif %}
     </div>
+  </div>
+</div>
+
+{# Object table #}
+<div class="row">
+  <div class="col col-md-12">
+    {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
+      {% if permissions.change or permissions.delete %}
+        <form method="post" class="form form-horizontal">
+          {% csrf_token %}
+          <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
+          <div class="table-responsive">
+            {% render_table table 'inc/table.html' %}
+          </div>
+          <div class="float-start noprint bulk-buttons">
+            {% block bulk_buttons %}{% endblock %}
+            {% if bulk_edit_url and permissions.change %}
+            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
+              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
+            </button>
+            {% endif %}
+            {% if bulk_delete_url and permissions.delete %}
+            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
+              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
+            </button>
+            {% endif %}
+          </div>
+        </form>
+      {% else %}
+        <div class="table-responsive">
+          {% render_table table 'inc/table.html' %}
+        </div>
+      {% endif %}
+    {% endwith %}
+    {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  </div>
 </div>
 {% table_config_form table table_name="ObjectTable" %}
 {% endblock content %}

+ 63 - 61
netbox/templates/inc/advanced_search.html

@@ -1,65 +1,67 @@
 {% load form_helpers %}
 {% load helpers %}
 
-<div class="collapse" id="advanced-search-content">
-    <form action="." method="get">
-        <div class="card">
-            <h5 class="card-header">
-                Advanced Search
-            </h5>
-            <div class="card-body overflow-visible d-flex flex-wrap justify-content-between py-3">
-                    {% for field in filter_form.hidden_fields %}
-                        {{ field }}
-                    {% endfor %}
-                    {% if filter_form.field_groups %}
-                        {% for group in filter_form.field_groups %}
-                            <div class="col">
-                                {% for name in group %}
-                                    {% with field=filter_form|get_item:name %}
-                                        {% if field|widget_type == 'checkboxinput' %}
-                                            <div class="form-check mb-3">
-                                                <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
-                                                {{ field }}
-                                            </div>
-                                        {% else %}
-                                            <div class="form-floating mb-3 mx-3">
-                                                {{ field }}
-                                                {{ field.label_tag }}
-                                            </div>
-                                        {% endif %}
-                                    {% endwith %}
-                                {% endfor %}
-                            </div>
-                        {% endfor %}
-                    {% else %}
-                        {% for field in filter_form.visible_fields %}
-                            <div class="col">
-                                {% if field|widget_type == 'checkboxinput' %}
-                                    <div class="form-check mb-3">
-                                        <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
-                                        {{ field }}
-                                    </div>
-                                {% else %}
-                                    <div class="form-floating mb-3">
-                                        {{ field }}
-                                        {{ field.label_tag }}
-                                    </div>
-                                {% endif %}
-                            </div>
-                        {% endfor %}
-                    {% endif %}
-            </div>
-            <div class="card-footer text-end noprint border-0">
-                <button type="button" class="btn btn-sm btn-outline-dark m-1" data-bs-toggle="collapse" data-bs-target="#advanced-search-content">
-                    <i class="mdi mdi-close"></i> Close
-                </button>
-                <button type="button" class="btn btn-sm btn-outline-danger m-1" data-reset-select>
-                    <i class="mdi mdi-backspace"></i> Reset
-                </button>
-                <button type="submit" class="btn btn-sm btn-primary m-1">
-                    <i class="mdi mdi-magnify"></i> Search
-                </button>
-            </div>
-        </div>
-    </form>
+<div id="advanced-search-content" class="collapse mb-3">
+  <div class="col col-md-12 noprint">
+      <form action="." method="get">
+          <div class="card">
+              <h5 class="card-header">
+                  Advanced Search
+              </h5>
+              <div class="card-body overflow-visible d-flex flex-wrap justify-content-between py-3">
+                      {% for field in filter_form.hidden_fields %}
+                          {{ field }}
+                      {% endfor %}
+                      {% if filter_form.field_groups %}
+                          {% for group in filter_form.field_groups %}
+                              <div class="col">
+                                  {% for name in group %}
+                                      {% with field=filter_form|get_item:name %}
+                                          {% if field|widget_type == 'checkboxinput' %}
+                                              <div class="form-check mb-3">
+                                                  <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                                  {{ field }}
+                                              </div>
+                                          {% else %}
+                                              <div class="form-floating mb-3 mx-3">
+                                                  {{ field }}
+                                                  {{ field.label_tag }}
+                                              </div>
+                                          {% endif %}
+                                      {% endwith %}
+                                  {% endfor %}
+                              </div>
+                          {% endfor %}
+                      {% else %}
+                          {% for field in filter_form.visible_fields %}
+                              <div class="col">
+                                  {% if field|widget_type == 'checkboxinput' %}
+                                      <div class="form-check mb-3">
+                                          <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                          {{ field }}
+                                      </div>
+                                  {% else %}
+                                      <div class="form-floating mb-3">
+                                          {{ field }}
+                                          {{ field.label_tag }}
+                                      </div>
+                                  {% endif %}
+                              </div>
+                          {% endfor %}
+                      {% endif %}
+              </div>
+              <div class="card-footer text-end noprint border-0">
+                  <button type="button" class="btn btn-sm btn-outline-dark m-1" data-bs-toggle="collapse" data-bs-target="#advanced-search-content">
+                      <i class="mdi mdi-close"></i> Close
+                  </button>
+                  <button type="button" class="btn btn-sm btn-outline-danger m-1" data-reset-select>
+                      <i class="mdi mdi-backspace"></i> Reset
+                  </button>
+                  <button type="submit" class="btn btn-sm btn-primary m-1">
+                      <i class="mdi mdi-magnify"></i> Search
+                  </button>
+              </div>
+          </div>
+      </form>
+  </div>
 </div>

+ 1 - 1
netbox/templates/inc/paginator.html

@@ -1,6 +1,6 @@
 {% load helpers %}
 
-<div class="paginator float-end text-end my-3">
+<div class="paginator float-end text-end">
     {% if paginator.num_pages > 1 %}
     <div class="btn-group btn-group-sm mb-3" role="group" aria-label="Pages">    
     {% if page.has_previous %}

+ 0 - 1
netbox/templates/inc/table.html

@@ -1,7 +1,6 @@
 {% load django_tables2 %}
 
 <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
-    <caption class="text-center small mt-3"></caption>
     {% if table.show_header %}
         <thead>
             <tr>

+ 16 - 14
netbox/utilities/templates/navigation/nav_items.html

@@ -21,7 +21,7 @@
           
           {% for group in menu.groups %}
             {# Within each main menu, there are groups of menu items #}
-            <div class="flex-column nav px-2">
+            <div class="flex-column nav">
               
               {% if menu.groups|length > 1 %}
                 <h6 class="accordion-item-title">{{ group.label }}</h6>
@@ -32,23 +32,25 @@
                 <div class="nav-item d-flex justify-content-between align-items-center">
                   
                   {# Menu Link with Text #}
-                  <a class="nav-link flex-grow-1 me-1" href="{% url item.url %}">
+                  <a class="nav-link flex-grow-1" href="{% url item.url %}">
                     {{ item.label }}
                   </a>
                   
                   {# Add & Import Buttons #}
-                  <div class="btn-group">
-                    {% 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>
+                  {% 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>
               {% endfor %}

+ 3 - 321
netbox/utilities/templatetags/nav.py

@@ -1,330 +1,12 @@
-from dataclasses import dataclass
-from typing import Dict, Sequence, Optional
+from typing import Dict
 from django import template
 from django.template import Context
 from django.contrib.auth.context_processors import PermWrapper
 
-register = template.Library()
-
-
-@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
-
-
-@dataclass
-class MenuGroup:
-    """A group of menu items within a menu."""
-
-    label: str
-    items: Sequence[MenuItem]
-
-
-@dataclass
-class Menu:
-    """A top level menu group. Example: Organization, Devices, IPAM."""
+from netbox.navigation_menu import Menu, MenuGroup, MENUS
 
-    label: str
-    icon: str
-    groups: Sequence[MenuGroup]
 
-
-ORGANIZATION_MENU = Menu(
-    label="Organization",
-    icon="domain",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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="Elevations", url="dcim:rack_elevation_list",
-                         add_url=None, import_url=None),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        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",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            label="Connections",
-            items=(
-                MenuItem(label="Cables", url="dcim:cable_list",
-                         add_url=None, import_url="dcim:cable_import"),
-                MenuItem(
-                    label="Console Connections", url="dcim:console_connections_list", add_url=None, import_url=None,
-                ),
-                MenuItem(
-                    label="Interface Connections", url="dcim:interface_connections_list", add_url=None, import_url=None,
-                ),
-                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",
-    groups=(
-        MenuGroup(
-            label="IP Addresses",
-            items=(
-                MenuItem(label="IP Addresses", url="ipam:ipaddress_list",
-                         add_url="ipam:ipaddress_add", import_url="ipam:ipaddress_import"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            label="Services",
-            items=(MenuItem(label="Services", url="ipam:service_list",
-                   add_url=None, import_url="ipam:service_import"),),
-        ),
-    ),
-)
-
-VIRTUALIZATION_MENU = Menu(
-    label="Virtualization",
-    icon="monitor",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-    ),
-)
-
-CIRCUITS_MENU = Menu(
-    label="Circuits",
-    icon="transit-connection-variant",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"
-                ),
-            ),
-        ),
-    ),
-)
-
-POWER_MENU = Menu(
-    label="Power",
-    icon="flash",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-    ),
-)
-
-OTHER_MENU = Menu(
-    label="Other",
-    icon="notification-clear-all",
-    groups=(
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            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"),
-            ),
-        ),
-        MenuGroup(
-            label="Miscellaneous",
-            items=(
-                MenuItem(label="Config Contexts",
-                         url="extras:configcontext_list", add_url=None, 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),
-            ),
-        ),
-    ),
-)
-
-MENUS = (
-    ORGANIZATION_MENU,
-    DEVICES_MENU,
-    IPAM_MENU,
-    VIRTUALIZATION_MENU,
-    CIRCUITS_MENU,
-    POWER_MENU,
-    OTHER_MENU,
-)
+register = template.Library()
 
 
 def process_menu(menu: Menu, perms: PermWrapper) -> MenuGroup: