Repository: nautobot/nautobot

Issue:
Indication other half of U is taken in rack elevation
### As ...

Isabella - Infrastructure Owner

### I want ...

See indication the other half of U is taken if there is a device taking only half U and I am looking at the empty half (from the other side).

### So that ...

I can correctly plan my racks, because now if I look at multiple rack elevations at once, I cannot see if any slot is completely free or the other half is actually occupied.



### I know this is done when...

- There is clear way of seeing this U is already partly occupied without looking at the other side


Example of current behavior from front and rear:
![image](https://user-images.githubusercontent.com/26114847/167578023-fc5fffce-c788-43d4-9226-1b1047f24495.png)
![image](https://user-images.githubusercontent.com/26114847/167578143-e75cfd1c-c57e-4f0e-814d-2b40f6c121ab.png)
Possible solution would be to show green stripes in 42 and 43 when looking from rear. This should work with current situation where `red` means the whole U is taken and `blue` is rack reservation.

### Optional - Feature groups this request pertains to.

- [ ] Automation
- [ ] Circuits
- [ ] DCIM
- [ ] IPAM
- [ ] Misc (including Data Sources)
- [ ] Organization
- [ ] Plugins (and other Extensibility)
- [ ] Security (Secrets, etc)
- [ ] Image Management
- [X] UI/UX
- [ ] Documentation
- [ ] Other (not directly a platform feature)

### Database Changes

None

### External Dependencies

None
Device labels hidden when images are enabled in Rack view
### Environment
* Nautobot version (Docker tag too if applicable): 1.5.4
* Python version: 3.9
* Database platform, version: Postgres 14
* Middleware(s): N/A

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Add a device to a Rack
2. Add a device name and image for the device 


<!-- What did you expect to happen? -->
### Expected Behavior
Expect the device name to show over the image

<!-- What happened instead? -->
### Observed Behavior
The device name is seen for a split second as the page loads.  Then the image is rendered over the device name.

With images enabled:
![image](https://user-images.githubusercontent.com/584942/206472372-a209c40a-51bb-4650-b13f-707b4a0100c5.png)
With images disabled:
![image](https://user-images.githubusercontent.com/584942/206472440-5fad8a31-28cc-4f37-adb7-7adfe0b35eb7.png)




Discussion:

Looking at the code, the primary cause is the order of images being added in `nautobot/dcim/elevations.py`. 

Putting the `if self.include_images` code before the `link.add()` statements get the labels on top.   However, they also need a shadow rendered, or the text is not always legible.  

This change: [https://github.com/lschweiss/nautobot/commit/5498c6cfdc617f24af23c61ce64ffd83d2cb752a](https://github.com/lschweiss/nautobot/commit/5498c6cfdc617f24af23c61ce64ffd83d2cb752a)

Renders this way:
![image](https://user-images.githubusercontent.com/584942/206487277-14184bb6-68a7-4f5d-a90e-587f2819ddf1.png)


I think an issue here is the legibility of your example which I believe is why this approach was chosen. However, maybe a text background can be applied to help with that.
I loaded the same data in Netbox.  It renders nicely creating shadows:
![image](https://user-images.githubusercontent.com/584942/206564376-3b8bd6c9-56b5-4b99-8b1e-06af70826285.png)

The code seems to have diverged quite a bit in elevations.py.  I'm not more than a python script kiddie, so I don't see an obvious way to duplicate this.  


Resolving patch:
diff --git a/nautobot/dcim/elevations.py b/nautobot/dcim/elevations.py
--- a/nautobot/dcim/elevations.py
+++ b/nautobot/dcim/elevations.py
@@ -4,7 +4,6 @@
 from django.urls import reverse
 from django.utils.http import urlencode
 
-from nautobot.utilities.utils import foreground_color
 from .choices import DeviceFaceChoices
 from .constants import RACK_ELEVATION_BORDER_WIDTH
 
@@ -46,13 +45,13 @@ def _get_device_description(device):
         )
 
     @staticmethod
-    def _add_gradient(drawing, id_, color):
+    def _add_gradient(drawing, id_, color, angle=45):
         gradient = drawing.linearGradient(
             start=(0, 0),
             end=(0, 25),
             spreadMethod="repeat",
             id_=id_,
-            gradientTransform="rotate(45, 0, 0)",
+            gradientTransform=f"rotate({angle}, 0, 0)",
             gradientUnits="userSpaceOnUse",
         )
         gradient.add_stop_color(offset="0%", color="#f7f7f7")
@@ -61,6 +60,16 @@ def _add_gradient(drawing, id_, color):
         gradient.add_stop_color(offset="100%", color=color)
         drawing.defs.add(gradient)
 
+    @staticmethod
+    def _add_filters(drawing):
+        new_filter = drawing.filter(id="darkmodeinvert")
+        fct = new_filter.feComponentTransfer(color_interpolation_filters="sRGB", result="inverted")
+        fct.feFuncR("table", tableValues="1,0")
+        fct.feFuncG("table", tableValues="1,0")
+        fct.feFuncB("table", tableValues="1,0")
+        new_filter.feColorMatrix(type_="hueRotate", values="180", in_="inverted")
+        drawing.defs.add(new_filter)
+
     @staticmethod
     def _setup_drawing(width, height):
         drawing = svgwrite.Drawing(size=(width, height))
@@ -73,6 +82,8 @@ def _setup_drawing(width, height):
         RackElevationSVG._add_gradient(drawing, "reserved", "#c7c7ff")
         RackElevationSVG._add_gradient(drawing, "occupied", "#d7d7d7")
         RackElevationSVG._add_gradient(drawing, "blocked", "#ffc0c0")
+        RackElevationSVG._add_gradient(drawing, "blocked_partial", "#a0a0a0", angle=-45)
+        RackElevationSVG._add_filters(drawing)
 
         return drawing
 
@@ -95,12 +106,24 @@ def _draw_device_front(self, drawing, device, start, end, text):
         )
         link.set_desc(self._get_device_description(device))
         link.add(drawing.rect(start, end, style=f"fill: #{color}", class_="slot"))
-        hex_color = f"#{foreground_color(color)}"
+
+        # Embed front device type image if one exists
+        if self.include_images and device.device_type.front_image:
+            image = drawing.image(
+                href=device.device_type.front_image.url,
+                insert=start,
+                size=end,
+                class_="device-image",
+            )
+            image.fit(scale="slice")
+            link.add(image)
+
         link.add(
             drawing.text(
                 device_fullname,
                 insert=text,
-                fill=hex_color,
+                fill="white",
+                style="text-shadow: 1px 1px 3px black;",
                 class_=f"rack-device-fullname{'' if self.display_fullname else ' hidden'}",
             )
         )
@@ -108,22 +131,12 @@ def _draw_device_front(self, drawing, device, start, end, text):
             drawing.text(
                 device_shortname,
                 insert=text,
-                fill=hex_color,
+                fill="white",
+                style="text-shadow: 1px 1px 3px black;",
                 class_=f"rack-device-shortname{' hidden' if self.display_fullname else ''}",
             )
         )
 
-        # Embed front device type image if one exists
-        if self.include_images and device.device_type.front_image:
-            image = drawing.image(
-                href=device.device_type.front_image.url,
-                insert=start,
-                size=end,
-                class_="device-image",
-            )
-            image.fit(scale="slice")
-            link.add(image)
-
     def _draw_device_rear(self, drawing, device, start, end, text):
         rect = drawing.rect(start, end, class_="slot blocked")
         rect.set_desc(self._get_device_description(device))
@@ -132,18 +145,6 @@ def _draw_device_rear(self, drawing, device, start, end, text):
         device_shortname = settings.UI_RACK_VIEW_TRUNCATE_FUNCTION(str(device))
 
         drawing.add(rect)
-        drawing.add(
-            drawing.text(
-                device_fullname, insert=text, class_=f"rack-device-fullname{'' if self.display_fullname else ' hidden'}"
-            )
-        )
-        drawing.add(
-            drawing.text(
-                device_shortname,
-                insert=text,
-                class_=f"rack-device-shortname{' hidden' if self.display_fullname else ''}",
-            )
-        )
 
         # Embed rear device type image if one exists
         if self.include_images and device.device_type.rear_image:
@@ -156,6 +157,25 @@ def _draw_device_rear(self, drawing, device, start, end, text):
             image.fit(scale="slice")
             drawing.add(image)
 
+        drawing.add(
+            drawing.text(
+                device_fullname,
+                insert=text,
+                fill="white",
+                style="text-shadow: 1px 1px 3px black;",
+                class_=f"rack-device-fullname{'' if self.display_fullname else ' hidden'}",
+            )
+        )
+        drawing.add(
+            drawing.text(
+                device_shortname,
+                insert=text,
+                fill="white",
+                style="text-shadow: 1px 1px 3px black;",
+                class_=f"rack-device-shortname{' hidden' if self.display_fullname else ''}",
+            )
+        )
+
     @staticmethod
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
         reverse_url = reverse("dcim:device_add")
@@ -189,7 +209,7 @@ def merge_elevations(self, face):
         unit_cursor = 0
         for u in elevation:
             o = other[unit_cursor]
-            if not u["device"] and o["device"] and o["device"].device_type.is_full_depth:
+            if not u["device"] and o["device"]:
                 u["device"] = o["device"]
                 u["height"] = 1
             unit_cursor += u.get("height", 1)
@@ -231,10 +251,24 @@ def render(self, face, unit_width, unit_height, legend_width):
             text_coordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
 
             # Draw the device
-            if device and device.face == face and device.pk in self.permitted_device_ids:
-                self._draw_device_front(drawing, device, start_coordinates, end_coordinates, text_coordinates)
-            elif device and device.device_type.is_full_depth and device.pk in self.permitted_device_ids:
-                self._draw_device_rear(drawing, device, start_coordinates, end_coordinates, text_coordinates)
+            if device and device.pk in self.permitted_device_ids:
+                if device.face == face:
+                    self._draw_device_front(drawing, device, start_coordinates, end_coordinates, text_coordinates)
+                elif device.device_type.is_full_depth:
+                    self._draw_device_rear(drawing, device, start_coordinates, end_coordinates, text_coordinates)
+                else:
+                    # Half-depth devices add an empty box that is partially blocked
+                    self._draw_empty(
+                        drawing,
+                        self.rack,
+                        start_coordinates,
+                        end_coordinates,
+                        text_coordinates,
+                        unit["id"],
+                        face,
+                        "slot blocked_partial",
+                        None,
+                    )
             elif device:
                 # Devices which the user does not have permission to view are rendered only as unavailable space
                 drawing.add(drawing.rect(start_coordinates, end_coordinates, class_="blocked"))


Test patch: