diff --git a/netbox/dcim/ui/panels.py b/netbox/dcim/ui/panels.py
index 0ed917c55..4db5e958c 100644
--- a/netbox/dcim/ui/panels.py
+++ b/netbox/dcim/ui/panels.py
@@ -23,6 +23,26 @@ class LocationPanel(panels.NestedGroupObjectPanel):
facility = attrs.TextAttr('facility', label=_('Facility'))
+class RackDimensionsPanel(panels.ObjectPanel):
+ form_factor = attrs.ChoiceAttr('form_factor', label=_('Form factor'))
+ width = attrs.ChoiceAttr('width', label=_('Width'))
+ u_height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
+ outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display', label=_('Outer width'))
+ outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display', label=_('Outer height'))
+ outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display', label=_('Outer depth'))
+ mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm', label=_('Mounting depth'))
+
+
+class RackNumberingPanel(panels.ObjectPanel):
+ starting_unit = attrs.TextAttr('starting_unit', label=_('Starting unit'))
+ desc_units = attrs.BooleanAttr('desc_units', label=_('Descending units'))
+
+
+class RackWeightPanel(panels.ObjectPanel):
+ weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display', label=_('Weight'))
+ max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight'))
+
+
class RackPanel(panels.ObjectPanel):
region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
@@ -40,6 +60,17 @@ class RackPanel(panels.ObjectPanel):
power_utilization = attrs.UtilizationAttr('get_power_utilization', label=_('Power utilization'))
+class RackRolePanel(panels.OrganizationalObjectPanel):
+ color = attrs.ColorAttr('color')
+
+
+class RackTypePanel(panels.ObjectPanel):
+ manufacturer = attrs.ObjectAttr('manufacturer', label=_('Manufacturer'), linkify=True)
+ model = attrs.TextAttr('model', label=_('Model'))
+ description = attrs.TextAttr('description', label=_('Description'))
+ airflow = attrs.ChoiceAttr('airflow', label=_('Airflow'))
+
+
class DevicePanel(panels.ObjectPanel):
region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 5e60f65a7..e825777f6 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -527,7 +527,7 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
layout = layout.Layout(
layout.Row(
layout.Column(
- panels.SitePanel(_('Site')),
+ panels.SitePanel(),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
@@ -817,6 +817,25 @@ class RackRoleListView(generic.ObjectListView):
@register_model_view(RackRole)
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackRole.objects.all()
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ panels.RackRolePanel(),
+ TagsPanel(),
+ PluginContentPanel('left_page'),
+ ),
+ layout.Column(
+ RelatedObjectsPanel(),
+ CustomFieldsPanel(),
+ PluginContentPanel('right_page'),
+ ),
+ ),
+ layout.Row(
+ layout.Column(
+ PluginContentPanel('full_width_page'),
+ ),
+ ),
+ )
def get_extra_context(self, request, instance):
return {
@@ -884,6 +903,29 @@ class RackTypeListView(generic.ObjectListView):
@register_model_view(RackType)
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackType.objects.all()
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ panels.RackTypePanel(),
+ panels.RackDimensionsPanel(_('Dimensions')),
+ TagsPanel(),
+ CommentsPanel(),
+ PluginContentPanel('left_page'),
+ ),
+ layout.Column(
+ panels.RackNumberingPanel(_('Numbering')),
+ panels.RackWeightPanel(_('Weight')),
+ CustomFieldsPanel(),
+ RelatedObjectsPanel(),
+ PluginContentPanel('right_page'),
+ ),
+ ),
+ layout.Row(
+ layout.Column(
+ PluginContentPanel('full_width_page'),
+ ),
+ ),
+ )
def get_extra_context(self, request, instance):
return {
diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py
index 2e931d714..4df8f64e1 100644
--- a/netbox/netbox/ui/attrs.py
+++ b/netbox/netbox/ui/attrs.py
@@ -13,12 +13,14 @@ from netbox.config import get_config
class Attr(ABC):
template_name = None
+ label = None
placeholder = mark_safe('—')
def __init__(self, accessor, label=None, template_name=None):
self.accessor = accessor
- self.label = label
self.template_name = template_name or self.template_name
+ if label is not None:
+ self.label = label
@abstractmethod
def render(self, obj, context=None):
@@ -37,9 +39,10 @@ class Attr(ABC):
class TextAttr(Attr):
template_name = 'ui/attrs/text.html'
- def __init__(self, *args, style=None, copy_button=False, **kwargs):
+ def __init__(self, *args, style=None, format_string=None, copy_button=False, **kwargs):
super().__init__(*args, **kwargs)
self.style = style
+ self.format_string = format_string
self.copy_button = copy_button
def render(self, obj, context=None):
@@ -47,6 +50,8 @@ class TextAttr(Attr):
value = self._resolve_attr(obj, self.accessor)
if value in (None, ''):
return self.placeholder
+ if self.format_string:
+ value = self.format_string.format(value)
return render_to_string(self.template_name, {
**context,
'value': value,
@@ -55,6 +60,28 @@ class TextAttr(Attr):
})
+class NumericAttr(Attr):
+ template_name = 'ui/attrs/numeric.html'
+
+ def __init__(self, *args, unit_accessor=None, copy_button=False, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.unit_accessor = unit_accessor
+ self.copy_button = copy_button
+
+ def render(self, obj, context=None):
+ context = context or {}
+ value = self._resolve_attr(obj, self.accessor)
+ if value in (None, ''):
+ return self.placeholder
+ unit = self._resolve_attr(obj, self.unit_accessor) if self.unit_accessor else None
+ return render_to_string(self.template_name, {
+ **context,
+ 'value': value,
+ 'unit': unit,
+ 'copy_button': self.copy_button,
+ })
+
+
class ChoiceAttr(Attr):
template_name = 'ui/attrs/choice.html'
@@ -77,6 +104,37 @@ class ChoiceAttr(Attr):
})
+class BooleanAttr(Attr):
+ template_name = 'ui/attrs/boolean.html'
+
+ def __init__(self, *args, display_false=True, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.display_false = display_false
+
+ def render(self, obj, context=None):
+ context = context or {}
+ value = self._resolve_attr(obj, self.accessor)
+ if value in (None, '') and not self.display_false:
+ return self.placeholder
+ return render_to_string(self.template_name, {
+ **context,
+ 'value': value,
+ })
+
+
+class ColorAttr(Attr):
+ template_name = 'ui/attrs/color.html'
+ label = _('Color')
+
+ def render(self, obj, context=None):
+ context = context or {}
+ value = self._resolve_attr(obj, self.accessor)
+ return render_to_string(self.template_name, {
+ **context,
+ 'color': value,
+ })
+
+
class ObjectAttr(Attr):
template_name = 'ui/attrs/object.html'
@@ -149,9 +207,9 @@ class AddressAttr(Attr):
class GPSCoordinatesAttr(Attr):
template_name = 'ui/attrs/gps_coordinates.html'
+ label = _('GPS Coordinates')
def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
- kwargs.setdefault('label', _('GPS Coordinates'))
super().__init__(accessor=None, **kwargs)
self.latitude_attr = latitude_attr
self.longitude_attr = longitude_attr
diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py
index 2ff495c42..d65c5ae2c 100644
--- a/netbox/netbox/ui/panels.py
+++ b/netbox/netbox/ui/panels.py
@@ -19,6 +19,7 @@ __all__ = (
'NestedGroupObjectPanel',
'ObjectPanel',
'ObjectsTablePanel',
+ 'OrganizationalObjectPanel',
'RelatedObjectsPanel',
'Panel',
'PluginContentPanel',
@@ -45,7 +46,7 @@ class Panel(ABC):
return render_to_string(self.template_name, {
'request': context.get('request'),
'object': obj,
- 'title': self.title,
+ 'title': self.title or title(obj._meta.verbose_name),
'actions': [action.get_context(obj) for action in self.actions],
**self.get_context(obj),
})
@@ -93,9 +94,12 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
}
-class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
+class OrganizationalObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
name = attrs.TextAttr('name', label=_('Name'))
description = attrs.TextAttr('description', label=_('Description'))
+
+
+class NestedGroupObjectPanel(OrganizationalObjectPanel, metaclass=ObjectPanelMeta):
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
diff --git a/netbox/templates/ui/attrs/boolean.html b/netbox/templates/ui/attrs/boolean.html
new file mode 100644
index 000000000..a724d687b
--- /dev/null
+++ b/netbox/templates/ui/attrs/boolean.html
@@ -0,0 +1 @@
+{% checkmark object.desc_units %}
diff --git a/netbox/templates/ui/attrs/color.html b/netbox/templates/ui/attrs/color.html
new file mode 100644
index 000000000..29d11207a
--- /dev/null
+++ b/netbox/templates/ui/attrs/color.html
@@ -0,0 +1 @@
+
diff --git a/netbox/templates/ui/attrs/numeric.html b/netbox/templates/ui/attrs/numeric.html
new file mode 100644
index 000000000..5c54f2979
--- /dev/null
+++ b/netbox/templates/ui/attrs/numeric.html
@@ -0,0 +1,12 @@
+{% load i18n %}
+
+ {{ value }}
+ {% if unit %}
+ {{ unit|lower }}
+ {% endif %}
+
+{% if copy_button %}
+
+
+
+{% endif %}