diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md
index c099d5115..6792e30a0 100644
--- a/docs/release-notes/version-2.6.md
+++ b/docs/release-notes/version-2.6.md
@@ -2,7 +2,9 @@
## Enhancements
+* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces
+* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
---
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index a5ce2811c..bbdbe251d 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -703,6 +703,34 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
)
+#
+# Rack elevations
+#
+
+class RackElevationFilterForm(RackFilterForm):
+ field_order = ['q', 'region', 'site', 'group_id', 'id', 'status', 'role', 'tenant_group', 'tenant']
+ id = ChainedModelChoiceField(
+ queryset=Rack.objects.all(),
+ label='Rack',
+ chains=(
+ ('site', 'site'),
+ ('group_id', 'group_id'),
+ ),
+ required=False,
+ widget=APISelectMultiple(
+ api_url='/api/dcim/racks/',
+ display_field='display_name',
+ )
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Filter the rack field based on the site and group
+ self.fields['site'].widget.add_filter_for('id', 'site')
+ self.fields['group_id'].widget.add_filter_for('id', 'group_id')
+
+
#
# Rack reservations
#
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 959e1043e..2d98515cf 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -388,7 +388,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
'page': page,
'total_count': total_count,
'face_id': face_id,
- 'filter_form': forms.RackFilterForm(request.GET),
+ 'filter_form': forms.RackElevationFilterForm(request.GET),
})
diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js
index 20d6c9126..0b8c7d719 100644
--- a/netbox/project-static/js/forms.js
+++ b/netbox/project-static/js/forms.js
@@ -398,4 +398,43 @@ $(document).ready(function() {
// Account for the header height when hash-scrolling
window.addEventListener('load', headerOffsetScroll);
window.addEventListener('hashchange', headerOffsetScroll);
+
+ // Offset between the preview window and the window edges
+ const IMAGE_PREVIEW_OFFSET_X = 20
+ const IMAGE_PREVIEW_OFFSET_Y = 10
+
+ // Preview an image attachment when the link is hovered over
+ $('a.image-preview').on('mouseover', function(e) {
+ // Twice the offset to account for all sides of the picture
+ var maxWidth = window.innerWidth - (e.clientX + (IMAGE_PREVIEW_OFFSET_X * 2));
+ var maxHeight = window.innerHeight - (e.clientY + (IMAGE_PREVIEW_OFFSET_Y * 2));
+ var img = $('').attr('id', 'image-preview-window').css({
+ display: 'none',
+ position: 'absolute',
+ maxWidth: maxWidth + 'px',
+ maxHeight: maxHeight + 'px',
+ left: e.pageX + IMAGE_PREVIEW_OFFSET_X + 'px',
+ top: e.pageY + IMAGE_PREVIEW_OFFSET_Y + 'px',
+ boxShadow: '0 0px 12px 3px rgba(0, 0, 0, 0.4)',
+ });
+
+ // Remove any existing preview windows and add the current one
+ $('#image-preview-window').remove();
+ $('body').append(img);
+
+ // Once loaded, show the preview if the image is indeed an image
+ img.on('load', function(e) {
+ if (e.target.complete && e.target.naturalWidth) {
+ $('#image-preview-window').fadeIn('fast');
+ }
+ });
+
+ // Begin loading
+ img.attr('src', e.target.href);
+ });
+
+ // Fade the image out; it will be deleted when another one is previewed
+ $('a.image-preview').on('mouseout', function() {
+ $('#image-preview-window').fadeOut('fast')
+ });
});
diff --git a/netbox/templates/inc/image_attachments.html b/netbox/templates/inc/image_attachments.html
index 1487b5349..2fee4dc78 100644
--- a/netbox/templates/inc/image_attachments.html
+++ b/netbox/templates/inc/image_attachments.html
@@ -10,7 +10,7 @@