Просмотр исходного кода

Merge branch 'develop' into feature

jeremystretch 4 лет назад
Родитель
Сommit
efb41b7433

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -17,7 +17,7 @@ body:
         What version of NetBox are you currently running? (If you don't have access to the most
         What version of NetBox are you currently running? (If you don't have access to the most
         recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
         recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
         before opening a bug report to see if your issue has already been addressed.)
         before opening a bug report to see if your issue has already been addressed.)
-      placeholder: v3.0.7
+      placeholder: v3.0.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
     attributes:
       label: NetBox version
       label: NetBox version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v3.0.7
+      placeholder: v3.0.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 0 - 1
docs/customization/custom-links.md

@@ -1 +0,0 @@
-{!models/extras/customlink.md!}

+ 22 - 1
docs/release-notes/version-3.0.md

@@ -1,6 +1,27 @@
 # NetBox v3.0
 # NetBox v3.0
 
 
-## v3.0.8 (FUTURE)
+## v3.0.9 (FUTURE)
+
+---
+
+## v3.0.8 (2021-10-20)
+
+### Enhancements
+
+* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
+* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
+
+### Bug Fixes
+
+* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
+* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
+* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
+* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
+* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
+* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
+* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
+* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
+* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
 
 
 ---
 ---
 
 

+ 1 - 1
mkdocs.yml

@@ -66,7 +66,7 @@ nav:
     - Customization:
     - Customization:
         - Custom Fields: 'customization/custom-fields.md'
         - Custom Fields: 'customization/custom-fields.md'
         - Custom Validation: 'customization/custom-validation.md'
         - Custom Validation: 'customization/custom-validation.md'
-        - Custom Links: 'customization/custom-links.md'
+        - Custom Links: 'models/extras/customlink.md'
         - Export Templates: 'customization/export-templates.md'
         - Export Templates: 'customization/export-templates.md'
         - Custom Scripts: 'customization/custom-scripts.md'
         - Custom Scripts: 'customization/custom-scripts.md'
         - Reports: 'customization/reports.md'
         - Reports: 'customization/reports.md'

+ 12 - 0
netbox/dcim/choices.py

@@ -704,6 +704,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
 # Interfaces
 # Interfaces
 #
 #
 
 
+class InterfaceKindChoices(ChoiceSet):
+    KIND_PHYSICAL = 'physical'
+    KIND_VIRTUAL = 'virtual'
+    KIND_WIRELESS = 'wireless'
+
+    CHOICES = (
+        (KIND_PHYSICAL, 'Physical'),
+        (KIND_VIRTUAL, 'Virtual'),
+        (KIND_WIRELESS, 'Wireless'),
+    )
+
+
 class InterfaceTypeChoices(ChoiceSet):
 class InterfaceTypeChoices(ChoiceSet):
 
 
     # Virtual
     # Virtual

+ 6 - 2
netbox/dcim/forms/filtersets.py

@@ -7,7 +7,6 @@ from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
-from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
     APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
     StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
     StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@@ -966,9 +965,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     model = Interface
     field_groups = [
     field_groups = [
         ['q', 'tag'],
         ['q', 'tag'],
-        ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
+        ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
     ]
     ]
+    kind = forms.MultipleChoiceField(
+        choices=InterfaceKindChoices,
+        required=False,
+        widget=StaticSelectMultiple()
+    )
     type = forms.MultipleChoiceField(
     type = forms.MultipleChoiceField(
         choices=InterfaceTypeChoices,
         choices=InterfaceTypeChoices,
         required=False,
         required=False,

+ 2 - 1
netbox/extras/filtersets.py

@@ -15,6 +15,7 @@ from .models import *
 __all__ = (
 __all__ = (
     'ConfigContextFilterSet',
     'ConfigContextFilterSet',
     'ContentTypeFilterSet',
     'ContentTypeFilterSet',
+    'CustomFieldFilterSet',
     'CustomLinkFilterSet',
     'CustomLinkFilterSet',
     'ExportTemplateFilterSet',
     'ExportTemplateFilterSet',
     'ImageAttachmentFilterSet',
     'ImageAttachmentFilterSet',
@@ -47,7 +48,7 @@ class WebhookFilterSet(BaseFilterSet):
         ]
         ]
 
 
 
 
-class CustomFieldFilterSet(django_filters.FilterSet):
+class CustomFieldFilterSet(BaseFilterSet):
     content_types = ContentTypeFilter()
     content_types = ContentTypeFilter()
 
 
     class Meta:
     class Meta:

+ 5 - 0
netbox/ipam/tables/ip.py

@@ -260,11 +260,16 @@ class IPRangeTable(BaseTable):
         linkify=True
         linkify=True
     )
     )
     tenant = TenantColumn()
     tenant = TenantColumn()
+    utilization = UtilizationColumn(
+        accessor='utilization',
+        orderable=False
+    )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = IPRange
         model = IPRange
         fields = (
         fields = (
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
+            'utilization',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
             'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',

+ 1 - 1
netbox/netbox/settings.py

@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
 # Environment setup
 # Environment setup
 #
 #
 
 
-VERSION = '3.0.8-dev'
+VERSION = '3.0.9-dev'
 
 
 # Hostname
 # Hostname
 HOSTNAME = platform.node()
 HOSTNAME = platform.node()

+ 1 - 1
netbox/netbox/views/__init__.py

@@ -137,7 +137,7 @@ class HomeView(View):
                 release_version, release_url = latest_release
                 release_version, release_url = latest_release
                 if release_version > version.parse(settings.VERSION):
                 if release_version > version.parse(settings.VERSION):
                     new_release = {
                     new_release = {
-                        'version': str(latest_release),
+                        'version': str(release_version),
                         'url': release_url,
                         'url': release_url,
                     }
                     }
 
 

+ 2 - 2
netbox/netbox/views/generic.py

@@ -282,11 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
                 messages.success(request, mark_safe(msg))
                 messages.success(request, mark_safe(msg))
 
 
                 if '_addanother' in request.POST:
                 if '_addanother' in request.POST:
-                    redirect_url = request.get_full_path()
+                    redirect_url = request.path
 
 
                     # If the object has clone_fields, pre-populate a new instance of the form
                     # If the object has clone_fields, pre-populate a new instance of the form
                     if hasattr(obj, 'clone_fields'):
                     if hasattr(obj, 'clone_fields'):
-                        redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
+                        redirect_url += f"?{prepare_cloned_fields(obj)}"
 
 
                     return redirect(redirect_url)
                     return redirect(redirect_url)
 
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/lldp.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/lldp.js.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-print.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 73 - 25
netbox/project-static/src/device/lldp.ts

@@ -1,6 +1,17 @@
 import { createToast } from '../bs';
 import { createToast } from '../bs';
 import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
 import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
 
 
+// Match an interface name that begins with a capital letter and is followed by at least one other
+// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
+const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
+
+// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
+// the first two characters).
+const CISCO_IOS_OVERRIDES = new Map<string, string>([
+  // Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
+  ['TwentyFiveGigE', 'Twe'],
+]);
+
 /**
 /**
  * Get an attribute from a row's cell.
  * Get an attribute from a row's cell.
  *
  *
@@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string
   return row.querySelector(query)?.getAttribute(attr) ?? null;
   return row.querySelector(query)?.getAttribute(attr) ?? null;
 }
 }
 
 
+/**
+ * Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
+ * interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
+ * would become `Gi0/1/2`.
+ *
+ * This should probably be replaced with something in the primary application (Django), such as
+ * a database field attached to given interface types. However, this is a temporary measure to
+ * replace the functionality of this one-liner:
+ *
+ * @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
+ *
+ * @param name Long-form/original interface name.
+ */
+function getInterfaceAlias(name: string | null): string | null {
+  if (name === null) {
+    return name;
+  }
+  if (name.match(CISCO_IOS_PATTERN)) {
+    // Extract the base name and numeric portions of the interface. For example, an input interface
+    // of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
+    const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
+
+    if (isTruthy(base) && isTruthy(numeric)) {
+      // Check the override map and use its value if the base name is present in the map.
+      // Otherwise, use the first two characters of the base name. For example,
+      // `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
+      // `Twe0/0/1`.
+      const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
+      return `${aliasBase}${numeric}`;
+    }
+  }
+  return name;
+}
+
 /**
 /**
  * Update row styles based on LLDP neighbor data.
  * Update row styles based on LLDP neighbor data.
  */
  */
@@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) {
 
 
     if (row !== null) {
     if (row !== null) {
       for (const neighbor of neighbors) {
       for (const neighbor of neighbors) {
-        const cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
-        const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
-        const cDevice = getData(row, 'td.configured_device', 'data');
-        const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
-        const cInterface = getData(row, 'td.configured_interface', 'data');
-
-        let cInterfaceShort = null;
-        if (isTruthy(cInterface)) {
-          cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2');
-        }
+        const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
+        const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
+        const configuredDevice = getData(row, 'td.configured_device', 'data');
+        const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
+        const configuredIface = getData(row, 'td.configured_interface', 'data');
+
+        const interfaceAlias = getInterfaceAlias(configuredIface);
 
 
-        const nHost = neighbor.remote_system_name ?? '';
-        const nPort = neighbor.remote_port ?? '';
-        const [nDevice] = nHost.split('.');
-        const [nInterface] = nPort.split('.');
+        const remoteName = neighbor.remote_system_name ?? '';
+        const remotePort = neighbor.remote_port ?? '';
+        const [neighborDevice] = remoteName.split('.');
+        const [neighborIface] = remotePort.split('.');
 
 
-        if (cellDevice !== null) {
-          cellDevice.innerText = nDevice;
+        if (deviceCell !== null) {
+          deviceCell.innerText = neighborDevice;
         }
         }
 
 
-        if (cellInterface !== null) {
-          cellInterface.innerText = nInterface;
+        if (interfaceCell !== null) {
+          interfaceCell.innerText = neighborIface;
         }
         }
 
 
-        if (!isTruthy(cDevice) && isTruthy(nDevice)) {
+        // Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
+        const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
+
+        // NetBox device or chassis matches LLDP neighbor.
+        const validNode =
+          configuredDevice === neighborDevice || configuredChassis === neighborDevice;
+
+        // NetBox configured interface matches LLDP neighbor interface.
+        const validInterface =
+          configuredIface === neighborIface || interfaceAlias === neighborIface;
+
+        if (nonConfiguredDevice) {
           row.classList.add('info');
           row.classList.add('info');
-        } else if (
-          (cDevice === nDevice || cChassis === nDevice) &&
-          cInterfaceShort === nInterface
-        ) {
-          row.classList.add('success');
-        } else if (cDevice === nDevice || cChassis === nDevice) {
+        } else if (validNode && validInterface) {
           row.classList.add('success');
           row.classList.add('success');
         } else {
         } else {
           row.classList.add('danger');
           row.classList.add('danger');

+ 2 - 4
netbox/project-static/src/sidenav.ts

@@ -266,10 +266,8 @@ class SideNav {
       for (const link of this.getActiveLinks()) {
       for (const link of this.getActiveLinks()) {
         this.activateLink(link, 'collapse');
         this.activateLink(link, 'collapse');
       }
       }
-      setTimeout(() => {
-        this.bodyRemove('hide');
-        this.bodyAdd('hidden');
-      }, 300);
+      this.bodyRemove('hide');
+      this.bodyAdd('hidden');
     }
     }
   }
   }
 
 

+ 7 - 1
netbox/project-static/styles/netbox.scss

@@ -197,9 +197,15 @@ table {
         text-decoration: underline;
         text-decoration: underline;
       }
       }
     }
     }
+    .dropdown {
+      // Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
+      // opened. See: https://github.com/twbs/bootstrap/issues/24251
+      position: static;
+    }
   }
   }
   th {
   th {
-    a, a:hover {
+    a,
+    a:hover {
       color: $body-color;
       color: $body-color;
       text-decoration: none;
       text-decoration: none;
     }
     }

+ 27 - 3
netbox/project-static/styles/sidenav.scss

@@ -105,6 +105,11 @@
   // Navbar brand
   // Navbar brand
   .sidenav-brand {
   .sidenav-brand {
     margin-right: 0;
     margin-right: 0;
+    transition: opacity 0.1s ease-in-out;
+  }
+
+  .sidenav-brand-icon {
+    transition: opacity 0.1s ease-in-out;
   }
   }
 
 
   .sidenav-inner {
   .sidenav-inner {
@@ -141,7 +146,17 @@
   }
   }
 
 
   .sidenav-toggle {
   .sidenav-toggle {
-    display: none;
+    // The sidenav toggle's default state is "hidden". Because modifying the `display` property
+    // isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
+    // to yield a similar result.
+    position: absolute;
+    display: inline-block;
+    opacity: 0;
+    // The transition itself is largely irrelevant, but CSS needs *something* to transition in
+    // order to apply a delay.
+    transition: opacity 10ms ease-in-out;
+    // Offset the transition delay so the icon isn't visible during the logo transition.
+    transition-delay: 0.1s;
   }
   }
 
 
   .sidenav-collapse {
   .sidenav-collapse {
@@ -350,13 +365,21 @@
   .sidenav-brand {
   .sidenav-brand {
     position: absolute;
     position: absolute;
     opacity: 0;
     opacity: 0;
-    transform: translateX(-150%);
   }
   }
 
 
   .sidenav-brand-icon {
   .sidenav-brand-icon {
     opacity: 1;
     opacity: 1;
   }
   }
 
 
+  .sidenav-toggle {
+    // Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
+    // with the logo elements.
+    opacity: 0;
+    position: absolute;
+    transition: unset;
+    transition-delay: 0ms;
+  }
+
   .navbar-nav > .nav-item {
   .navbar-nav > .nav-item {
     > .nav-link {
     > .nav-link {
       &:after {
       &:after {
@@ -402,7 +425,8 @@
 
 
   @include media-breakpoint-up(lg) {
   @include media-breakpoint-up(lg) {
     .sidenav-toggle {
     .sidenav-toggle {
-      display: inline-block;
+      position: relative;
+      opacity: 1;
     }
     }
   }
   }
 }
 }

+ 1 - 0
netbox/project-static/styles/theme-dark.scss

@@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300;
 
 
 // Forms
 // Forms
 $component-active-bg: $primary;
 $component-active-bg: $primary;
+$component-active-color: $black;
 $form-text-color: $text-muted;
 $form-text-color: $text-muted;
 $input-bg: $gray-900;
 $input-bg: $gray-900;
 $input-disabled-bg: $gray-700;
 $input-disabled-bg: $gray-700;

+ 1 - 1
netbox/templates/dcim/devicetype.html

@@ -143,7 +143,7 @@
     </div>
     </div>
     <div class="row my-3">
     <div class="row my-3">
         <div class="col col-md-12">
         <div class="col col-md-12">
-            <ul class="nav nav-pills" role="tablist">
+            <ul class="nav nav-pills mb-1" role="tablist">
                 <li class="nav-item" role="presentation">
                 <li class="nav-item" role="presentation">
                     <button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
                     <button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
                         Interfaces {% badge interface_table.rows|length %}
                         Interfaces {% badge interface_table.rows|length %}

+ 5 - 5
netbox/templates/extras/objectchange.html

@@ -130,12 +130,12 @@
             </h5>
             </h5>
             <div class="card-body">
             <div class="card-body">
                 {% if object.postchange_data %}
                 {% if object.postchange_data %}
-                <pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
-                    <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
-                    {% endspaceless %}{% endfor %}
-                </pre>
+                    <pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
+                        <span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
+                        {% endspaceless %}{% endfor %}
+                    </pre>
                 {% else %}
                 {% else %}
-                <span class="text-muted">None</span>
+                    <span class="text-muted">None</span>
                 {% endif %}
                 {% endif %}
             </div>
             </div>
         </div>
         </div>

+ 2 - 2
netbox/templates/extras/webhook.html

@@ -47,7 +47,7 @@
           <tr>
           <tr>
             <th scope="row">Update</th>
             <th scope="row">Update</th>
             <td>
             <td>
-              {% if object.type_create %}
+              {% if object.type_update %}
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
               {% else %}
               {% else %}
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>
@@ -57,7 +57,7 @@
           <tr>
           <tr>
             <th scope="row">Delete</th>
             <th scope="row">Delete</th>
             <td>
             <td>
-              {% if object.type_create %}
+              {% if object.type_delete %}
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
                 <i class="mdi mdi-check-bold text-success" title="Yes"></i>
               {% else %}
               {% else %}
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>
                 <i class="mdi mdi-close-thick text-danger" title="No"></i>

+ 12 - 9
netbox/templates/generic/object.html

@@ -6,9 +6,17 @@
 {% load plugins %}
 {% load plugins %}
 
 
 {% block header %}
 {% block header %}
-  {# Breadcrumbs #}
-  <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
-    <div class="float-end">
+  <div class="d-flex justify-content-between align-items-center">
+    {# Breadcrumbs #}
+    <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
+      <ol class="breadcrumb">
+        {% block breadcrumbs %}
+          <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
+        {% endblock breadcrumbs %}
+      </ol>
+    </nav>
+    {# Object identifier #}
+    <div class="float-end px-3">
         <code class="text-muted">
         <code class="text-muted">
           {% block object_identifier %}
           {% block object_identifier %}
             {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
             {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
@@ -16,12 +24,7 @@
           {% endblock object_identifier %}
           {% endblock object_identifier %}
         </code>
         </code>
     </div>
     </div>
-    <ol class="breadcrumb">
-      {% block breadcrumbs %}
-        <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
-      {% endblock breadcrumbs %}
-    </ol>
-  </nav>
+  </div>
   {{ block.super }}
   {{ block.super }}
 {% endblock %}
 {% endblock %}
 
 

+ 35 - 33
netbox/templates/inc/table.html

@@ -1,41 +1,43 @@
 {% load django_tables2 %}
 {% load django_tables2 %}
 
 
-<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
+<div class="table-responsive">
+  <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
     {% if table.show_header %}
     {% if table.show_header %}
-        <thead>
-            <tr>
-                {% for column in table.columns %}
-                    {% if column.orderable %}
-                        <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
-                    {% else %}
-                        <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
-                    {% endif %}
-                {% endfor %}
-            </tr>
-        </thead>
+      <thead>
+        <tr>
+          {% for column in table.columns %}
+            {% if column.orderable %}
+              <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
+            {% else %}
+              <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
+            {% endif %}
+          {% endfor %}
+        </tr>
+      </thead>
     {% endif %}
     {% endif %}
     <tbody>
     <tbody>
-        {% for row in table.page.object_list|default:table.rows %}
-            <tr {{ row.attrs.as_html }}>
-                {% for column, cell in row.items %}
-                    <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
-                {% endfor %}
-            </tr>
-        {% empty %}
-            {% if table.empty_text %}
-                <tr>
-                    <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
-                </tr>
-            {% endif %}
-        {% endfor %}
+      {% for row in table.page.object_list|default:table.rows %}
+        <tr {{ row.attrs.as_html }}>
+          {% for column, cell in row.items %}
+            <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
+          {% endfor %}
+        </tr>
+      {% empty %}
+        {% if table.empty_text %}
+          <tr>
+            <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
+          </tr>
+        {% endif %}
+      {% endfor %}
     </tbody>
     </tbody>
     {% if table.has_footer %}
     {% if table.has_footer %}
-        <tfoot>
-            <tr>
-                {% for column in table.columns %}
-                    <td>{{ column.footer }}</td>
-                {% endfor %}
-            </tr>
-        </tfoot>
+      <tfoot>
+        <tr>
+          {% for column in table.columns %}
+            <td>{{ column.footer }}</td>
+          {% endfor %}
+        </tr>
+      </tfoot>
     {% endif %}
     {% endif %}
-</table>
+  </table>
+</div>

+ 1 - 1
netbox/utilities/templatetags/helpers.py

@@ -58,7 +58,7 @@ def render_json(value):
     """
     """
     Render a dictionary as formatted JSON.
     Render a dictionary as formatted JSON.
     """
     """
-    return json.dumps(value, indent=4, sort_keys=True)
+    return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
 
 
 
 
 @register.filter()
 @register.filter()

+ 3 - 3
requirements.txt

@@ -18,11 +18,11 @@ gunicorn==20.1.0
 Jinja2==3.0.2
 Jinja2==3.0.2
 Markdown==3.3.4
 Markdown==3.3.4
 markdown-include==0.6.0
 markdown-include==0.6.0
-mkdocs-material==7.3.2
+mkdocs-material==7.3.4
 netaddr==0.8.0
 netaddr==0.8.0
-Pillow==8.3.2
+Pillow==8.4.0
 psycopg2-binary==2.9.1
 psycopg2-binary==2.9.1
-PyYAML==5.4.1
+PyYAML==6.0
 svgwrite==1.4.1
 svgwrite==1.4.1
 tablib==3.0.0
 tablib==3.0.0
 
 

Некоторые файлы не были показаны из-за большого количества измененных файлов