Skip to content

Commit 40eb733

Browse files
committed
Display device names in front of device front/rear images
Fixes #6879
1 parent e8fb86a commit 40eb733

File tree

7 files changed

+108
-78
lines changed

7 files changed

+108
-78
lines changed

netbox/dcim/svg.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ def _draw_device_front(self, drawing, device, start, end, text):
9999
)
100100
link.set_desc(self._get_device_description(device))
101101
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
102-
hex_color = '#{}'.format(foreground_color(color))
103-
link.add(drawing.text(str(name), insert=text, fill=hex_color))
102+
text_color = '#{}'.format(foreground_color(color))
104103

105104
# Embed front device type image if one exists
106105
if self.include_images and device.device_type.front_image:
@@ -112,12 +111,19 @@ def _draw_device_front(self, drawing, device, start, end, text):
112111
)
113112
image.fit(scale='slice')
114113
link.add(image)
114+
label_class = 'device-label-image'
115+
link.add(drawing.text(str(name), insert=text, stroke='#{}'.format(color),
116+
stroke_width='0.2em', stroke_linejoin='round', class_=label_class))
117+
else:
118+
label_class = 'device-label-noimage'
119+
120+
# Add label
121+
link.add(drawing.text(str(name), insert=text, fill=text_color, class_=label_class))
115122

116123
def _draw_device_rear(self, drawing, device, start, end, text):
117124
rect = drawing.rect(start, end, class_="slot blocked")
118125
rect.set_desc(self._get_device_description(device))
119126
drawing.add(rect)
120-
drawing.add(drawing.text(str(device), insert=text))
121127

122128
# Embed rear device type image if one exists
123129
if self.include_images and device.device_type.rear_image:
@@ -129,6 +135,14 @@ def _draw_device_rear(self, drawing, device, start, end, text):
129135
)
130136
image.fit(scale='slice')
131137
drawing.add(image)
138+
label_class = 'device-label-image'
139+
else:
140+
label_class = 'device-label-noimage'
141+
142+
# Add label
143+
drawing.add(drawing.text(str(device), insert=text, stroke='white',
144+
stroke_width='0.2em', stroke_linejoin='round', class_=label_class))
145+
drawing.add(drawing.text(str(device), insert=text, class_=label_class))
132146

133147
@staticmethod
134148
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):

netbox/project-static/dist/netbox.js

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

netbox/project-static/dist/netbox.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

netbox/project-static/src/racks.ts

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,99 @@
1-
import { rackImagesState } from './stores';
1+
import { rackImagesState, RackViewSelection } from './stores';
22
import { getElements } from './util';
33

44
import type { StateManager } from './state';
55

6-
type RackToggleState = { hidden: boolean };
6+
export type RackViewState = { view: RackViewSelection };
77

88
/**
9-
* Toggle the Rack Image button to reflect the current state. If the current state is hidden and
10-
* the images are therefore hidden, the button should say "Show Images". Likewise, if the current
11-
* state is *not* hidden, and therefore the images are shown, the button should say "Hide Images".
12-
*
13-
* @param hidden Current State - `true` if images are hidden, `false` otherwise.
14-
* @param button Button element.
9+
* Show or hide images and labels to build the desired rack view.
1510
*/
16-
function toggleRackImagesButton(hidden: boolean, button: HTMLButtonElement): void {
17-
const text = hidden ? 'Show Images' : 'Hide Images';
18-
const selected = hidden ? '' : 'selected';
19-
button.setAttribute('selected', selected);
20-
button.innerHTML = `<i class="mdi mdi-file-image-outline"></i>&nbsp;${text}`;
11+
function setRackView(
12+
view: RackViewSelection,
13+
elevation: HTMLObjectElement,
14+
): void {
15+
switch(view) {
16+
case 'images-and-labels': {
17+
showRackElements('image.device-image', elevation);
18+
showRackElements('text.device-label-image', elevation);
19+
showRackElements('text.device-label-noimage', elevation);
20+
break;
21+
}
22+
case 'images-or-labels': {
23+
showRackElements('image.device-image', elevation);
24+
hideRackElements('text.device-label-image', elevation);
25+
showRackElements('text.device-label-noimage', elevation);
26+
break;
27+
}
28+
case 'images-only': {
29+
showRackElements('image.device-image', elevation);
30+
hideRackElements('text.device-label-image', elevation);
31+
hideRackElements('text.device-label-noimage', elevation);
32+
break;
33+
}
34+
case 'labels-only': {
35+
hideRackElements('image.device-image', elevation);
36+
showRackElements('text.device-label-image', elevation);
37+
showRackElements('text.device-label-noimage', elevation);
38+
break;
39+
}
40+
}
2141
}
2242

23-
/**
24-
* Show all rack images.
25-
*/
26-
function showRackImages(): void {
27-
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
28-
const images = elevation.contentDocument?.querySelectorAll('image.device-image') ?? [];
29-
for (const image of images) {
30-
image.classList.remove('hidden');
31-
}
43+
function showRackElements(
44+
selector: string,
45+
elevation: HTMLObjectElement,
46+
): void {
47+
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
48+
for (const element of elements) {
49+
element.classList.remove('hidden');
3250
}
3351
}
3452

35-
/**
36-
* Hide all rack images.
37-
*/
38-
function hideRackImages(): void {
39-
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
40-
const images = elevation.contentDocument?.querySelectorAll('image.device-image') ?? [];
41-
for (const image of images) {
42-
image.classList.add('hidden');
43-
}
53+
function hideRackElements(
54+
selector: string,
55+
elevation: HTMLObjectElement,
56+
): void {
57+
const elements = elevation.contentDocument?.querySelectorAll(selector) ?? [];
58+
for (const element of elements) {
59+
element.classList.add('hidden');
4460
}
4561
}
4662

4763
/**
48-
* Toggle the visibility of device images and update the toggle button style.
64+
* Change the visibility of all racks in response to selection.
4965
*/
50-
function handleRackImageToggle(
51-
target: HTMLButtonElement,
52-
state: StateManager<RackToggleState>,
66+
function handleRackViewSelect(
67+
newView: RackViewSelection,
68+
state: StateManager<RackViewState>,
5369
): void {
54-
const initiallyHidden = state.get('hidden');
55-
state.set('hidden', !initiallyHidden);
56-
const hidden = state.get('hidden');
57-
58-
if (hidden) {
59-
hideRackImages();
60-
} else {
61-
showRackImages();
70+
state.set('view', newView);
71+
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
72+
setRackView(newView, elevation);
6273
}
63-
toggleRackImagesButton(hidden, target);
6474
}
6575

6676
/**
67-
* Add onClick callback for toggling rack elevation images. Synchronize the image toggle button
68-
* text and display state of images with the local state.
77+
* Add change callback for selecting rack elevation images, and set
78+
* initial state of select and the images themselves
6979
*/
7080
export function initRackElevation() {
71-
const initiallyHidden = rackImagesState.get('hidden');
72-
for (const button of getElements<HTMLButtonElement>('button.toggle-images')) {
73-
toggleRackImagesButton(initiallyHidden, button);
81+
const initialView = rackImagesState.get('view');
7482

75-
button.addEventListener(
76-
'click',
83+
for (const control of getElements<HTMLSelectElement>('select.rack-view')) {
84+
control.selectedIndex = [...control.options].findIndex(o => o.value == initialView);
85+
control.addEventListener(
86+
'change',
7787
event => {
78-
handleRackImageToggle(event.currentTarget as HTMLButtonElement, rackImagesState);
88+
handleRackViewSelect((event.currentTarget as any).value as RackViewSelection, rackImagesState);
7989
},
8090
false,
8191
);
8292
}
93+
8394
for (const element of getElements<HTMLObjectElement>('.rack_elevation')) {
8495
element.addEventListener('load', () => {
85-
if (initiallyHidden) {
86-
hideRackImages();
87-
} else if (!initiallyHidden) {
88-
showRackImages();
89-
}
96+
setRackView(initialView, element);
9097
});
9198
}
9299
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createState } from '../state';
22

3-
export const rackImagesState = createState<{ hidden: boolean }>(
4-
{ hidden: false },
3+
export type RackViewSelection = 'images-and-labels' | 'images-or-labels' | 'images-only' | 'labels-only';
4+
5+
export const rackImagesState = createState<{ view: RackViewSelection }>(
6+
{ view: 'images-and-labels' },
57
{ persist: true },
68
);

netbox/templates/dcim/rack.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
{% endblock %}
1919

2020
{% block extra_controls %}
21-
<button class="btn btn-sm btn-outline-primary toggle-images" selected="selected">
22-
<i class="mdi mdi-file-image-outline"></i>
23-
Hide Images
24-
</button>
21+
<select class="btn btn-sm btn-outline-dark rack-view">
22+
<option value="images-and-labels" selected="selected">Images and Labels</option>
23+
<option value="images-or-labels">Images or Labels</option>
24+
<option value="images-only">Images only</option>
25+
<option value="labels-only">Labels only</option>
26+
</select>
2527
<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 %}">
2628
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
2729
</a>

netbox/templates/dcim/rack_elevation_list.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
{% block controls %}
88
<div class="controls">
99
<div class="control-group">
10-
<button class="btn btn-sm btn-outline-dark toggle-images" selected="selected">
11-
<span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
12-
</button>
10+
<div class="btn-group btn-group-sm" role="group">
11+
<select class="btn btn-sm btn-outline-secondary active rack-view">
12+
<option value="images-and-labels" selected="selected">Images and Labels</option>
13+
<option value="images-or-labels">Images or Labels</option>
14+
<option value="images-only">Images only</option>
15+
<option value="labels-only">Labels only</option>
16+
</select>
17+
</div>
1318
<div class="btn-group btn-group-sm" role="group">
1419
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
1520
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>

0 commit comments

Comments
 (0)