mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
66c4d23119
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
||||
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/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v3.0.6
|
||||
placeholder: v3.0.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.6
|
||||
placeholder: v3.0.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -1,8 +1,8 @@
|
||||
# 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:
|
||||
|
||||
|
@ -1,5 +1,21 @@
|
||||
# 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)
|
||||
|
||||
### Enhancements
|
||||
|
@ -2,7 +2,7 @@ import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
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 drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
@ -17,10 +17,10 @@ from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.exceptions import ServiceUnavailable
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.views import ModelViewSet
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related, decode_dict
|
||||
from virtualization.models import VirtualMachine
|
||||
@ -675,15 +675,25 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
if not peer_device_name or not peer_interface_name:
|
||||
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(
|
||||
Interface.objects.all(),
|
||||
device__name=peer_device_name,
|
||||
Interface.objects.restrict(request.user, 'view'),
|
||||
device=peer_device,
|
||||
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
|
||||
|
@ -192,6 +192,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
@ -210,6 +211,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
(TYPE_USB_MICRO_AB, 'USB Micro AB'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_OTHER, 'Other'),
|
||||
@ -337,6 +339,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_USB_MICRO_AB = 'usb-micro-ab'
|
||||
TYPE_USB_3_B = 'usb-3-b'
|
||||
TYPE_USB_3_MICROB = 'usb-3-micro-b'
|
||||
# Direct current (DC)
|
||||
@ -444,6 +447,7 @@ class PowerPortTypeChoices(ChoiceSet):
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(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_MICROB, 'USB 3.0 Micro B'),
|
||||
)),
|
||||
|
@ -480,12 +480,21 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='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):
|
||||
|
||||
|
@ -112,6 +112,9 @@ class RackElevationSVG:
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
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):
|
||||
rect = drawing.rect(start, end, class_="slot blocked")
|
||||
@ -129,6 +132,9 @@ class RackElevationSVG:
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
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
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@ -1490,40 +1491,35 @@ class ConnectedDeviceTest(APITestCase):
|
||||
|
||||
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(
|
||||
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(
|
||||
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.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.save()
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_get_connected_device(self):
|
||||
|
||||
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.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):
|
||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.0.6'
|
||||
VERSION = '3.0.7'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
@ -282,14 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
messages.success(request, mark_safe(msg))
|
||||
|
||||
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 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)
|
||||
|
||||
@ -880,6 +877,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
initial_data['device'] = request.GET.get('device')
|
||||
elif 'device_type' in request.GET:
|
||||
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)
|
||||
restrict_form_fields(form, request.user)
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,92 +1,90 @@
|
||||
import { rackImagesState } from './stores';
|
||||
import { rackImagesState, RackViewSelection } from './stores';
|
||||
import { getElements } from './util';
|
||||
|
||||
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> ${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the visibility of device images and update the toggle button style.
|
||||
*/
|
||||
function handleRackImageToggle(
|
||||
target: HTMLButtonElement,
|
||||
state: StateManager<RackToggleState>,
|
||||
function setRackView(
|
||||
view: RackViewSelection,
|
||||
elevation: HTMLObjectElement,
|
||||
): void {
|
||||
const initiallyHidden = state.get('hidden');
|
||||
state.set('hidden', !initiallyHidden);
|
||||
const hidden = state.get('hidden');
|
||||
|
||||
if (hidden) {
|
||||
hideRackImages();
|
||||
} else {
|
||||
showRackImages();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showRackElements(
|
||||
selector: string,
|
||||
elevation: HTMLObjectElement,
|
||||
): void {
|
||||
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
|
||||
for (const element of elements) {
|
||||
element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideRackElements(
|
||||
selector: string,
|
||||
elevation: HTMLObjectElement,
|
||||
): void {
|
||||
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
|
||||
for (const element of elements) {
|
||||
element.classList.add('hidden');
|
||||
}
|
||||
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.
|
||||
* Change the visibility of all racks in response to selection.
|
||||
*/
|
||||
function handleRackViewSelect(
|
||||
newView: RackViewSelection,
|
||||
state: StateManager<RackViewState>,
|
||||
): void {
|
||||
state.set('view', newView);
|
||||
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
|
||||
setRackView(newView, elevation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add change callback for selecting rack elevation images, and set
|
||||
* initial state of select and the images themselves
|
||||
*/
|
||||
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 => {
|
||||
handleRackImageToggle(event.currentTarget as HTMLButtonElement, rackImagesState);
|
||||
handleRackViewSelect((event.currentTarget as any).value as RackViewSelection, rackImagesState);
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
for (const element of getElements<HTMLObjectElement>('.rack_elevation')) {
|
||||
element.addEventListener('load', () => {
|
||||
if (initiallyHidden) {
|
||||
hideRackImages();
|
||||
} else if (!initiallyHidden) {
|
||||
showRackImages();
|
||||
}
|
||||
setRackView(initialView, element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
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 },
|
||||
);
|
||||
|
@ -18,10 +18,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}">
|
||||
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
|
||||
</a>
|
||||
@ -271,6 +267,13 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<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="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
|
@ -7,9 +7,13 @@
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<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">
|
||||
<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>
|
||||
|
@ -3,6 +3,10 @@
|
||||
|
||||
{% block title %}{{ report.name }}{% endblock %}
|
||||
|
||||
{% block object_identifier %}
|
||||
{{ report.full_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% 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' %}#module.{{ report.module }}">{{ report.module|bettertitle }}</a></li>
|
||||
|
@ -5,6 +5,10 @@
|
||||
|
||||
{% block title %}{{ script }}{% endblock %}
|
||||
|
||||
{% block object_identifier %}
|
||||
{{ script.full_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% 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' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
|
||||
|
@ -9,15 +9,17 @@
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<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>
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
{% endblock %}
|
||||
{% endblock breadcrumbs %}
|
||||
</ol>
|
||||
</nav>
|
||||
{{ block.super }}
|
||||
|
@ -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">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
|
||||
</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
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -18,7 +18,7 @@ gunicorn==20.1.0
|
||||
Jinja2==3.0.2
|
||||
Markdown==3.3.4
|
||||
markdown-include==0.6.0
|
||||
mkdocs-material==7.3.1
|
||||
mkdocs-material==7.3.2
|
||||
netaddr==0.8.0
|
||||
Pillow==8.3.2
|
||||
psycopg2-binary==2.9.1
|
||||
|
Loading…
Reference in New Issue
Block a user