Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #4434: Enable highlighting devices within rack elevations #9606

Merged
merged 1 commit into from
Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/version-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
* [#4350](https://github.com/netbox-community/netbox/issues/4350) - Illustrate reservations vertically alongside rack elevations
* [#4434](https://github.com/netbox-community/netbox/issues/4434) - Enable highlighting devices within rack elevations
* [#5303](https://github.com/netbox-community/netbox/issues/5303) - A virtual machine may be assigned to a site and/or cluster
* [#7120](https://github.com/netbox-community/netbox/issues/7120) - Add `termination_date` field to Circuit
* [#7744](https://github.com/netbox-community/netbox/issues/7744) - Add `status` field to Location
Expand Down
11 changes: 10 additions & 1 deletion netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ def elevation(self, request, pk=None):
data = serializer.validated_data

if data['render'] == 'svg':
# Determine attributes for highlighting devices (if any)
highlight_params = []
for param in request.GET.getlist('highlight'):
try:
highlight_params.append(param.split(':', 1))
except ValueError:
pass

# Render and return the elevation as an SVG drawing with the correct content type
drawing = rack.get_elevation_svg(
face=data['face'],
Expand All @@ -223,7 +231,8 @@ def elevation(self, request, pk=None):
unit_height=data['unit_height'],
legend_width=data['legend_width'],
include_images=data['include_images'],
base_url=request.build_absolute_uri('/')
base_url=request.build_absolute_uri('/'),
highlight_params=highlight_params
)
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')

Expand Down
6 changes: 4 additions & 2 deletions netbox/dcim/models/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ def get_elevation_svg(
legend_width=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH,
margin_width=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH,
include_images=True,
base_url=None
base_url=None,
highlight_params=None
):
"""
Return an SVG of the rack elevation
Expand All @@ -394,7 +395,8 @@ def get_elevation_svg(
margin_width=margin_width,
user=user,
include_images=include_images,
base_url=base_url
base_url=base_url,
highlight_params=highlight_params
)

return elevation.render(face)
Expand Down
57 changes: 39 additions & 18 deletions netbox/dcim/svg/racks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from svgwrite.text import Text

from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Q
from django.urls import reverse
from django.utils.http import urlencode

from netbox.config import get_config
from utilities.utils import foreground_color, array_to_ranges
from dcim.choices import DeviceFaceChoices
from dcim.constants import RACK_ELEVATION_BORDER_WIDTH


Expand Down Expand Up @@ -51,12 +52,17 @@ class RackElevationSVG:
Use this class to render a rack elevation as an SVG image.

:param rack: A NetBox Rack instance
:param unit_width: Rendered unit width, in pixels
:param unit_height: Rendered unit height, in pixels
:param legend_width: Legend width, in pixels (where the unit labels appear)
:param margin_width: Margin width, in pixels (where reservations appear)
:param user: User instance. If specified, only devices viewable by this user will be fully displayed.
:param include_images: If true, the SVG document will embed front/rear device face images, where available
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
:param highlight_params: Iterable of two-tuples which identifies attributes of devices to highlight
"""
def __init__(self, rack, unit_height=None, unit_width=None, legend_width=None, margin_width=None, user=None,
include_images=True, base_url=None):
include_images=True, base_url=None, highlight_params=None):
self.rack = rack
self.include_images = include_images
self.base_url = base_url.rstrip('/') if base_url is not None else ''
Expand All @@ -74,6 +80,17 @@ def __init__(self, rack, unit_height=None, unit_width=None, legend_width=None, m
permitted_devices = permitted_devices.restrict(user, 'view')
self.permitted_device_ids = permitted_devices.values_list('pk', flat=True)

# Determine device(s) to highlight within the elevation (if any)
self.highlight_devices = []
if highlight_params:
q = Q()
for k, v in highlight_params:
q |= Q(**{k: v})
try:
self.highlight_devices = permitted_devices.filter(q)
except FieldError:
pass

@staticmethod
def _add_gradient(drawing, id_, color):
gradient = LinearGradient(
Expand Down Expand Up @@ -123,40 +140,44 @@ def _get_device_coords(self, position, height):
def _draw_device(self, device, coords, size, color=None, image=None):
name = get_device_name(device)
description = get_device_description(device)
text_color = f'#{foreground_color(color)}' if color else '#000000'
text_coords = (
coords[0] + size[0] / 2,
coords[1] + size[1] / 2
)
text_color = f'#{foreground_color(color)}' if color else '#000000'

# Determine whether highlighting is in use, and if so, whether to shade this device
is_shaded = self.highlight_devices and device not in self.highlight_devices
css_extra = ' shaded' if is_shaded else ''

# Create hyperlink element
link = Hyperlink(
href='{}{}'.format(
self.base_url,
reverse('dcim:device', kwargs={'pk': device.pk})
),
target='_blank',
)
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target='_blank')
link.set_desc(description)

# Add rect element to hyperlink
if color:
link.add(Rect(coords, size, style=f'fill: #{color}', class_='slot'))
link.add(Rect(coords, size, style=f'fill: #{color}', class_=f'slot{css_extra}'))
else:
link.add(Rect(coords, size, class_='slot blocked'))
link.add(Text(name, insert=text_coords, fill=text_color))
link.add(Rect(coords, size, class_=f'slot blocked{css_extra}'))
link.add(Text(name, insert=text_coords, fill=text_color, class_=f'label{css_extra}'))

# Embed device type image if provided
if self.include_images and image:
image = Image(
href='{}{}'.format(self.base_url, image.url),
href=f'{self.base_url}{image.url}',
insert=coords,
size=size,
class_='device-image'
class_=f'device-image{css_extra}'
)
image.fit(scale='slice')
link.add(image)
link.add(Text(name, insert=text_coords, stroke='black',
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
link.add(Text(name, insert=text_coords, fill='white', class_='device-image-label'))
link.add(
Text(name, insert=text_coords, stroke='black', stroke_width='0.2em', stroke_linejoin='round',
class_=f'device-image-label{css_extra}')
)
link.add(
Text(name, insert=text_coords, fill='white', class_=f'device-image-label{css_extra}')
)

self.drawing.add(link)

Expand Down
6 changes: 6 additions & 0 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,13 +639,19 @@ def get_extra_context(self, request, instance):

device_count = Device.objects.restrict(request.user, 'view').filter(rack=instance).count()

# Determine any additional parameters to pass when embedding the rack elevations
svg_extra = '&'.join([
f'highlight=id:{pk}' for pk in request.GET.getlist('device')
])

return {
'device_count': device_count,
'reservations': reservations,
'power_feeds': power_feeds,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
'svg_extra': svg_extra,
}


Expand Down
2 changes: 1 addition & 1 deletion netbox/project-static/dist/rack_elevation.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions netbox/project-static/styles/rack-elevation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ svg {
visibility: hidden;
}

rect.shaded, image.shaded {
opacity: 25%;
}
text.shaded {
opacity: 50%;
}

// Rack elevation container.
.rack {
fill: none;
Expand Down
5 changes: 5 additions & 0 deletions netbox/templates/dcim/device.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ <h5 class="card-header">
<td>
{% if object.rack %}
<a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack }}</a>
<div class="float-end noprint">
<a href="{% url 'dcim:rack' pk=object.rack.pk %}?device={{ object.pk }}" class="btn btn-primary btn-sm" title="Highlight device">
<i class="mdi mdi-view-day-outline"></i>
</a>
</div>
{% else %}
{{ ''|placeholder }}
{% endif %}
Expand Down
4 changes: 2 additions & 2 deletions netbox/templates/dcim/inc/rack_elevation.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<div style="margin-left: -30px">
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg" class="rack_elevation"></object>
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation"></object>
</div>
<div class="text-center mt-3">
<a class="btn btn-outline-primary btn-sm" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg">
<a class="btn btn-outline-primary btn-sm" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}">
<i class="mdi mdi-file-download"></i> Download SVG
</a>
</div>
4 changes: 2 additions & 2 deletions netbox/templates/dcim/rack.html
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,13 @@ <h5 class="card-header">
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Front</h4>
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
{% include 'dcim/inc/rack_elevation.html' with face='front' extra_params=svg_extra %}
</div>
</div>
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Rear</h4>
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
{% include 'dcim/inc/rack_elevation.html' with face='rear' extra_params=svg_extra %}
</div>
</div>
</div>
Expand Down