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 %}