2
0
Эх сурвалжийг харах

Merge pull request #7510 from netbox-community/develop

Release v3.0.7
Jeremy Stretch 4 жил өмнө
parent
commit
66c4d23119

+ 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.6
+      placeholder: v3.0.7
     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.6
+      placeholder: v3.0.7
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 2 - 2
docs/models/extras/customlink.md

@@ -1,8 +1,8 @@
 # Custom Links
 # Custom Links
 
 
-Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a network monitoring system.
+Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
 
 
-Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link is assigned text and a URL, both of which support Jinja2 templating. The text and URL are rendered with the context variable `obj` representing the current object.
+Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the Netbox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `obj`, and custom fields through `obj.cf`.
 
 
 For example, you might define a link like this:
 For example, you might define a link like this:
 
 

+ 16 - 0
docs/release-notes/version-3.0.md

@@ -1,5 +1,21 @@
 # NetBox v3.0
 # NetBox v3.0
 
 
+## v3.0.7 (2021-10-08)
+
+### Enhancements
+
+* [#6879](https://github.com/netbox-community/netbox/issues/6879) - Improve ability to toggle images/labels in rack elevations 
+* [#7485](https://github.com/netbox-community/netbox/issues/7485) - Add USB micro AB type
+
+### Bug Fixes
+
+* [#7051](https://github.com/netbox-community/netbox/issues/7051) - Fix permissions evaluation and improve error handling for connected device REST API endpoint
+* [#7471](https://github.com/netbox-community/netbox/issues/7471) - Correct redirect URL when attaching images via "add another" button
+* [#7474](https://github.com/netbox-community/netbox/issues/7474) - Fix AttributeError exception when rendering a report or custom script
+* [#7479](https://github.com/netbox-community/netbox/issues/7479) - Fix parent interface choices when bulk editing VM interfaces
+
+---
+
 ## v3.0.6 (2021-10-06)
 ## v3.0.6 (2021-10-06)
 
 
 ### Enhancements
 ### Enhancements

+ 19 - 9
netbox/dcim/api/views.py

@@ -2,7 +2,7 @@ import socket
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django.conf import settings
 from django.conf import settings
-from django.http import HttpResponseForbidden, HttpResponse
+from django.http import Http404, HttpResponse, HttpResponseForbidden
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from drf_yasg import openapi
 from drf_yasg import openapi
 from drf_yasg.openapi import Parameter
 from drf_yasg.openapi import Parameter
@@ -17,10 +17,10 @@ from dcim import filtersets
 from dcim.models import *
 from dcim.models import *
 from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
 from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
 from ipam.models import Prefix, VLAN
 from ipam.models import Prefix, VLAN
-from netbox.api.views import ModelViewSet
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.exceptions import ServiceUnavailable
 from netbox.api.exceptions import ServiceUnavailable
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
+from netbox.api.views import ModelViewSet
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from utilities.utils import count_related, decode_dict
 from utilities.utils import count_related, decode_dict
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
@@ -675,15 +675,25 @@ class ConnectedDeviceViewSet(ViewSet):
         if not peer_device_name or not peer_interface_name:
         if not peer_device_name or not peer_interface_name:
             raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
             raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
 
 
-        # Determine local interface from peer interface's connection
+        # Determine local endpoint from peer interface's connection
+        peer_device = get_object_or_404(
+            Device.objects.restrict(request.user, 'view'),
+            name=peer_device_name
+        )
         peer_interface = get_object_or_404(
         peer_interface = get_object_or_404(
-            Interface.objects.all(),
-            device__name=peer_device_name,
+            Interface.objects.restrict(request.user, 'view'),
+            device=peer_device,
             name=peer_interface_name
             name=peer_interface_name
         )
         )
-        local_interface = peer_interface.connected_endpoint
+        endpoint = peer_interface.connected_endpoint
 
 
-        if local_interface is None:
-            return Response()
+        # If an Interface, return the parent device
+        if type(endpoint) is Interface:
+            device = get_object_or_404(
+                Device.objects.restrict(request.user, 'view'),
+                pk=endpoint.device_id
+            )
+            return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
 
 
-        return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)
+        # Connected endpoint is none or not an Interface
+        raise Http404

+ 4 - 0
netbox/dcim/choices.py

@@ -192,6 +192,7 @@ class ConsolePortTypeChoices(ChoiceSet):
     TYPE_USB_MINI_B = 'usb-mini-b'
     TYPE_USB_MINI_B = 'usb-mini-b'
     TYPE_USB_MICRO_A = 'usb-micro-a'
     TYPE_USB_MICRO_A = 'usb-micro-a'
     TYPE_USB_MICRO_B = 'usb-micro-b'
     TYPE_USB_MICRO_B = 'usb-micro-b'
+    TYPE_USB_MICRO_AB = 'usb-micro-ab'
     TYPE_OTHER = 'other'
     TYPE_OTHER = 'other'
 
 
     CHOICES = (
     CHOICES = (
@@ -210,6 +211,7 @@ class ConsolePortTypeChoices(ChoiceSet):
             (TYPE_USB_MINI_B, 'USB Mini B'),
             (TYPE_USB_MINI_B, 'USB Mini B'),
             (TYPE_USB_MICRO_A, 'USB Micro A'),
             (TYPE_USB_MICRO_A, 'USB Micro A'),
             (TYPE_USB_MICRO_B, 'USB Micro B'),
             (TYPE_USB_MICRO_B, 'USB Micro B'),
+            (TYPE_USB_MICRO_AB, 'USB Micro AB'),
         )),
         )),
         ('Other', (
         ('Other', (
             (TYPE_OTHER, 'Other'),
             (TYPE_OTHER, 'Other'),
@@ -337,6 +339,7 @@ class PowerPortTypeChoices(ChoiceSet):
     TYPE_USB_MINI_B = 'usb-mini-b'
     TYPE_USB_MINI_B = 'usb-mini-b'
     TYPE_USB_MICRO_A = 'usb-micro-a'
     TYPE_USB_MICRO_A = 'usb-micro-a'
     TYPE_USB_MICRO_B = 'usb-micro-b'
     TYPE_USB_MICRO_B = 'usb-micro-b'
+    TYPE_USB_MICRO_AB = 'usb-micro-ab'
     TYPE_USB_3_B = 'usb-3-b'
     TYPE_USB_3_B = 'usb-3-b'
     TYPE_USB_3_MICROB = 'usb-3-micro-b'
     TYPE_USB_3_MICROB = 'usb-3-micro-b'
     # Direct current (DC)
     # Direct current (DC)
@@ -444,6 +447,7 @@ class PowerPortTypeChoices(ChoiceSet):
             (TYPE_USB_MINI_B, 'USB Mini B'),
             (TYPE_USB_MINI_B, 'USB Mini B'),
             (TYPE_USB_MICRO_A, 'USB Micro A'),
             (TYPE_USB_MICRO_A, 'USB Micro A'),
             (TYPE_USB_MICRO_B, 'USB Micro B'),
             (TYPE_USB_MICRO_B, 'USB Micro B'),
+            (TYPE_USB_MICRO_AB, 'USB Micro AB'),
             (TYPE_USB_3_B, 'USB 3.0 Type B'),
             (TYPE_USB_3_B, 'USB 3.0 Type B'),
             (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
             (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
         )),
         )),

+ 9 - 0
netbox/dcim/filtersets.py

@@ -480,12 +480,21 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
 
 
 
 
 class DeviceTypeComponentFilterSet(django_filters.FilterSet):
 class DeviceTypeComponentFilterSet(django_filters.FilterSet):
+    q = django_filters.CharFilter(
+        method='search',
+        label='Search',
+    )
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         field_name='device_type_id',
         field_name='device_type_id',
         label='Device type (ID)',
         label='Device type (ID)',
     )
     )
 
 
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        return queryset.filter(name__icontains=value)
+
 
 
 class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
 
 

+ 6 - 0
netbox/dcim/svg.py

@@ -112,6 +112,9 @@ class RackElevationSVG:
             )
             )
             image.fit(scale='slice')
             image.fit(scale='slice')
             link.add(image)
             link.add(image)
+            link.add(drawing.text(str(name), insert=text, stroke='black',
+                     stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
+            link.add(drawing.text(str(name), insert=text, fill='white', class_='device-image-label'))
 
 
     def _draw_device_rear(self, drawing, device, start, end, text):
     def _draw_device_rear(self, drawing, device, start, end, text):
         rect = drawing.rect(start, end, class_="slot blocked")
         rect = drawing.rect(start, end, class_="slot blocked")
@@ -129,6 +132,9 @@ class RackElevationSVG:
             )
             )
             image.fit(scale='slice')
             image.fit(scale='slice')
             drawing.add(image)
             drawing.add(image)
+            drawing.add(drawing.text(str(device), insert=text, stroke='black',
+                        stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
+            drawing.add(drawing.text(str(device), insert=text, fill='white', class_='device-image-label'))
 
 
     @staticmethod
     @staticmethod
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):

+ 16 - 20
netbox/dcim/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
+from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
@@ -1490,40 +1491,35 @@ class ConnectedDeviceTest(APITestCase):
 
 
         super().setUp()
         super().setUp()
 
 
-        self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
-        self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
-        manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
-        self.devicetype1 = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
-        )
-        self.devicetype2 = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Test Device Type 2', slug='test-device-type-2'
-        )
-        self.devicerole1 = DeviceRole.objects.create(
-            name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
-        )
-        self.devicerole2 = DeviceRole.objects.create(
-            name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
-        )
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1', color='ff0000')
         self.device1 = Device.objects.create(
         self.device1 = Device.objects.create(
-            device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice1', site=self.site1
+            device_type=devicetype, device_role=devicerole, name='TestDevice1', site=site
         )
         )
         self.device2 = Device.objects.create(
         self.device2 = Device.objects.create(
-            device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice2', site=self.site1
+            device_type=devicetype, device_role=devicerole, name='TestDevice2', site=site
         )
         )
         self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
         self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
         self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
         self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
+        self.interface3 = Interface.objects.create(device=self.device1, name='eth1')  # Not connected
 
 
         cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
         cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
         cable.save()
         cable.save()
 
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_get_connected_device(self):
     def test_get_connected_device(self):
-
         url = reverse('dcim-api:connected-device-list')
         url = reverse('dcim-api:connected-device-list')
-        response = self.client.get(url + '?peer_device=TestDevice2&peer_interface=eth0', **self.header)
 
 
+        url_params = f'?peer_device={self.device1.name}&peer_interface={self.interface1.name}'
+        response = self.client.get(url + url_params, **self.header)
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertHttpStatus(response, status.HTTP_200_OK)
-        self.assertEqual(response.data['name'], self.device1.name)
+        self.assertEqual(response.data['name'], self.device2.name)
+
+        url_params = f'?peer_device={self.device1.name}&peer_interface={self.interface3.name}'
+        response = self.client.get(url + url_params, **self.header)
+        self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
 
 
 
 
 class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
 class VirtualChassisTest(APIViewTestCases.APIViewTestCase):

+ 1 - 1
netbox/netbox/settings.py

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

+ 4 - 5
netbox/netbox/views/generic.py

@@ -282,14 +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.path
-                    return_url = request.GET.get('return_url')
-                    if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
-                        redirect_url = f'{redirect_url}?return_url={return_url}'
+                    redirect_url = request.get_full_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 return_url else '?'}{prepare_cloned_fields(obj)}"
+                        redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
 
 
                     return redirect(redirect_url)
                     return redirect(redirect_url)
 
 
@@ -880,6 +877,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
                 initial_data['device'] = request.GET.get('device')
                 initial_data['device'] = request.GET.get('device')
             elif 'device_type' in request.GET:
             elif 'device_type' in request.GET:
                 initial_data['device_type'] = request.GET.get('device_type')
                 initial_data['device_type'] = request.GET.get('device_type')
+            elif 'virtual_machine' in request.GET:
+                initial_data['virtual_machine'] = request.GET.get('virtual_machine')
 
 
             form = self.form(model, initial=initial_data)
             form = self.form(model, initial=initial_data)
             restrict_form_fields(form, request.user)
             restrict_form_fields(form, request.user)

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
netbox/project-static/dist/netbox.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 55 - 57
netbox/project-static/src/racks.ts

@@ -1,92 +1,90 @@
-import { rackImagesState } from './stores';
+import { rackImagesState, RackViewSelection } from './stores';
 import { getElements } from './util';
 import { getElements } from './util';
 
 
 import type { StateManager } from './state';
 import type { StateManager } from './state';
 
 
-type RackToggleState = { hidden: boolean };
+export type RackViewState = { view: RackViewSelection };
 
 
 /**
 /**
- * Toggle the Rack Image button to reflect the current state. If the current state is hidden and
- * the images are therefore hidden, the button should say "Show Images". Likewise, if the current
- * state is *not* hidden, and therefore the images are shown, the button should say "Hide Images".
- *
- * @param hidden Current State - `true` if images are hidden, `false` otherwise.
- * @param button Button element.
+ * Show or hide images and labels to build the desired rack view.
  */
  */
-function toggleRackImagesButton(hidden: boolean, button: HTMLButtonElement): void {
-  const text = hidden ? 'Show Images' : 'Hide Images';
-  const selected = hidden ? '' : 'selected';
-  button.setAttribute('selected', selected);
-  button.innerHTML = `<i class="mdi mdi-file-image-outline"></i>&nbsp;${text}`;
+function setRackView(
+  view: RackViewSelection,
+  elevation: HTMLObjectElement,
+): void {
+  switch(view) {
+    case 'images-and-labels': {
+      showRackElements('image.device-image', elevation);
+      showRackElements('text.device-image-label', elevation);
+      break;
+    }
+    case 'images-only': {
+      showRackElements('image.device-image', elevation);
+      hideRackElements('text.device-image-label', elevation);
+      break;
+    }
+    case 'labels-only': {
+      hideRackElements('image.device-image', elevation);
+      hideRackElements('text.device-image-label', elevation);
+      break;
+    }
+  }
 }
 }
 
 
-/**
- * Show all rack images.
- */
-function showRackImages(): void {
-  for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
-    const images = elevation.contentDocument?.querySelectorAll('image.device-image') ?? [];
-    for (const image of images) {
-      image.classList.remove('hidden');
-    }
+function showRackElements(
+  selector: string,
+  elevation: HTMLObjectElement,
+): void {
+  const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
+  for (const element of elements) {
+    element.classList.remove('hidden');
   }
   }
 }
 }
 
 
-/**
- * Hide all rack images.
- */
-function hideRackImages(): void {
-  for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
-    const images = elevation.contentDocument?.querySelectorAll('image.device-image') ?? [];
-    for (const image of images) {
-      image.classList.add('hidden');
-    }
+function hideRackElements(
+  selector: string,
+  elevation: HTMLObjectElement,
+): void {
+  const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
+  for (const element of elements) {
+    element.classList.add('hidden');
   }
   }
 }
 }
 
 
 /**
 /**
- * Toggle the visibility of device images and update the toggle button style.
+ * Change the visibility of all racks in response to selection.
  */
  */
-function handleRackImageToggle(
-  target: HTMLButtonElement,
-  state: StateManager<RackToggleState>,
+function handleRackViewSelect(
+  newView: RackViewSelection,
+  state: StateManager<RackViewState>,
 ): void {
 ): void {
-  const initiallyHidden = state.get('hidden');
-  state.set('hidden', !initiallyHidden);
-  const hidden = state.get('hidden');
-
-  if (hidden) {
-    hideRackImages();
-  } else {
-    showRackImages();
+  state.set('view', newView);
+  for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
+    setRackView(newView, elevation);
   }
   }
-  toggleRackImagesButton(hidden, target);
 }
 }
 
 
 /**
 /**
- * Add onClick callback for toggling rack elevation images. Synchronize the image toggle button
- * text and display state of images with the local state.
+ * Add change callback for selecting rack elevation images, and set
+ * initial state of select and the images themselves
  */
  */
 export function initRackElevation(): void {
 export function initRackElevation(): void {
-  const initiallyHidden = rackImagesState.get('hidden');
-  for (const button of getElements<HTMLButtonElement>('button.toggle-images')) {
-    toggleRackImagesButton(initiallyHidden, button);
+  const initialView = rackImagesState.get('view');
 
 
-    button.addEventListener(
-      'click',
+  for (const control of getElements<HTMLSelectElement>('select.rack-view')) {
+    control.selectedIndex = [...control.options].findIndex(o => o.value == initialView);
+    control.addEventListener(
+      'change',
       event => {
       event => {
-        handleRackImageToggle(event.currentTarget as HTMLButtonElement, rackImagesState);
+        handleRackViewSelect((event.currentTarget as any).value as RackViewSelection, rackImagesState);
       },
       },
       false,
       false,
     );
     );
   }
   }
+
   for (const element of getElements<HTMLObjectElement>('.rack_elevation')) {
   for (const element of getElements<HTMLObjectElement>('.rack_elevation')) {
     element.addEventListener('load', () => {
     element.addEventListener('load', () => {
-      if (initiallyHidden) {
-        hideRackImages();
-      } else if (!initiallyHidden) {
-        showRackImages();
-      }
+      setRackView(initialView, element);
     });
     });
   }
   }
 }
 }

+ 4 - 2
netbox/project-static/src/stores/rackImages.ts

@@ -1,6 +1,8 @@
 import { createState } from '../state';
 import { createState } from '../state';
 
 
-export const rackImagesState = createState<{ hidden: boolean }>(
-  { hidden: false },
+export type RackViewSelection = 'images-and-labels' | 'images-only' | 'labels-only';
+
+export const rackImagesState = createState<{ view: RackViewSelection }>(
+  { view: 'images-and-labels' },
   { persist: true },
   { persist: true },
 );
 );

+ 7 - 4
netbox/templates/dcim/rack.html

@@ -18,10 +18,6 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
-  <button class="btn btn-sm btn-outline-primary toggle-images" selected="selected">
-    <i class="mdi mdi-file-image-outline"></i> 
-    Hide Images
-  </button>
   <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
   <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
     <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
     <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
   </a>
   </a>
@@ -271,6 +267,13 @@
         {% plugin_left_page object %}
         {% plugin_left_page object %}
 	  </div>
 	  </div>
     <div class="col col-12 col-xl-7">
     <div class="col col-12 col-xl-7">
+      <div class="text-end mb-4">
+        <select class="btn btn-sm btn-outline-dark rack-view">
+          <option value="images-and-labels" selected="selected">Images and Labels</option>
+          <option value="images-only">Images only</option>
+          <option value="labels-only">Labels only</option>
+        </select>
+      </div>
         <div class="row" style="margin-bottom: 20px">
         <div class="row" style="margin-bottom: 20px">
             <div class="col col-md-6 col-sm-6 col-xs-12 text-center">
             <div class="col col-md-6 col-sm-6 col-xs-12 text-center">
               <div style="margin-left: 30px">
               <div style="margin-left: 30px">

+ 7 - 3
netbox/templates/dcim/rack_elevation_list.html

@@ -7,9 +7,13 @@
 {% block controls %}
 {% block controls %}
     <div class="controls">
     <div class="controls">
         <div class="control-group">
         <div class="control-group">
-            <button class="btn btn-sm btn-outline-dark toggle-images" selected="selected">
-                <span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
-            </button>
+            <div class="btn-group btn-group-sm" role="group">
+                <select class="btn btn-sm btn-outline-secondary rack-view">
+                  <option value="images-and-labels" selected="selected">Images and Labels</option>
+                  <option value="images-only">Images only</option>
+                  <option value="labels-only">Labels only</option>
+                </select>
+            </div>
             <div class="btn-group btn-group-sm" role="group">
             <div class="btn-group btn-group-sm" role="group">
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>

+ 4 - 0
netbox/templates/extras/report.html

@@ -3,6 +3,10 @@
 
 
 {% block title %}{{ report.name }}{% endblock %}
 {% block title %}{{ report.name }}{% endblock %}
 
 
+{% block object_identifier %}
+  {{ report.full_name }}
+{% endblock %}
+
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}">Reports</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:report_list' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>

+ 4 - 0
netbox/templates/extras/script.html

@@ -5,6 +5,10 @@
 
 
 {% block title %}{{ script }}{% endblock %}
 {% block title %}{{ script }}{% endblock %}
 
 
+{% block object_identifier %}
+  {{ script.full_name }}
+{% endblock %}
+
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
   <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>

+ 7 - 5
netbox/templates/generic/object.html

@@ -9,15 +9,17 @@
   {# Breadcrumbs #}
   {# Breadcrumbs #}
   <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
   <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
     <div class="float-end">
     <div class="float-end">
-      <code class="text-muted" title="Object type and ID">
-        {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
-        {% if object.slug %}({{ object.slug }}){% endif %}
-      </code>
+        <code class="text-muted">
+          {% block object_identifier %}
+            {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
+            {% if object.slug %}({{ object.slug }}){% endif %}
+          {% endblock object_identifier %}
+        </code>
     </div>
     </div>
     <ol class="breadcrumb">
     <ol class="breadcrumb">
       {% block breadcrumbs %}
       {% block breadcrumbs %}
         <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
         <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
-      {% endblock %}
+      {% endblock breadcrumbs %}
     </ol>
     </ol>
   </nav>
   </nav>
   {{ block.super }}
   {{ block.super }}

+ 1 - 1
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -13,7 +13,7 @@
             <button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
             <button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
                 <span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
                 <span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
             </button>
             </button>
-            <button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtualmachine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
                 <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
                 <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
             </button>
             </button>
         {% endif %}
         {% endif %}

+ 1 - 1
requirements.txt

@@ -18,7 +18,7 @@ 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.1
+mkdocs-material==7.3.2
 netaddr==0.8.0
 netaddr==0.8.0
 Pillow==8.3.2
 Pillow==8.3.2
 psycopg2-binary==2.9.1
 psycopg2-binary==2.9.1

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно