Просмотр исходного кода

Implement layout declaration under view

Jeremy Stretch 3 месяцев назад
Родитель
Сommit
3fd4664a76

+ 6 - 6
netbox/dcim/ui/panels.py

@@ -1,9 +1,9 @@
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
-from netbox.ui import attrs, components
+from netbox.ui import attrs, panels
 
 
 
 
-class SitePanel(components.ObjectPanel):
+class SitePanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('region', label=_('Region'), linkify=True)
     region = attrs.NestedObjectAttr('region', label=_('Region'), linkify=True)
     group = attrs.NestedObjectAttr('group', label=_('Group'), linkify=True)
     group = attrs.NestedObjectAttr('group', label=_('Group'), linkify=True)
     status = attrs.ChoiceAttr('status', label=_('Status'))
     status = attrs.ChoiceAttr('status', label=_('Status'))
@@ -16,14 +16,14 @@ class SitePanel(components.ObjectPanel):
     gps_coordinates = attrs.GPSCoordinatesAttr()
     gps_coordinates = attrs.GPSCoordinatesAttr()
 
 
 
 
-class LocationPanel(components.NestedGroupObjectPanel):
+class LocationPanel(panels.NestedGroupObjectPanel):
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     status = attrs.ChoiceAttr('status', label=_('Status'))
     status = attrs.ChoiceAttr('status', label=_('Status'))
     tenant = attrs.ObjectAttr('tenant', label=_('Tenant'), linkify=True, grouped_by='group')
     tenant = attrs.ObjectAttr('tenant', label=_('Tenant'), linkify=True, grouped_by='group')
     facility = attrs.TextAttr('facility', label=_('Facility'))
     facility = attrs.TextAttr('facility', label=_('Facility'))
 
 
 
 
-class RackPanel(components.ObjectPanel):
+class RackPanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
@@ -40,7 +40,7 @@ class RackPanel(components.ObjectPanel):
     power_utilization = attrs.UtilizationAttr('get_power_utilization', label=_('Power utilization'))
     power_utilization = attrs.UtilizationAttr('get_power_utilization', label=_('Power utilization'))
 
 
 
 
-class DevicePanel(components.ObjectPanel):
+class DevicePanel(panels.ObjectPanel):
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
     location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
@@ -61,7 +61,7 @@ class DevicePanel(components.ObjectPanel):
     config_template = attrs.ObjectAttr('config_template', label=_('Config template'), linkify=True)
     config_template = attrs.ObjectAttr('config_template', label=_('Config template'), linkify=True)
 
 
 
 
-class DeviceManagementPanel(components.ObjectPanel):
+class DeviceManagementPanel(panels.ObjectPanel):
     status = attrs.ChoiceAttr('status', label=_('Status'))
     status = attrs.ChoiceAttr('status', label=_('Status'))
     role = attrs.NestedObjectAttr('role', label=_('Role'), linkify=True, max_depth=3)
     role = attrs.NestedObjectAttr('role', label=_('Role'), linkify=True, max_depth=3)
     platform = attrs.NestedObjectAttr('platform', label=_('Platform'), linkify=True, max_depth=3)
     platform = attrs.NestedObjectAttr('platform', label=_('Platform'), linkify=True, max_depth=3)

+ 11 - 4
netbox/dcim/views.py

@@ -17,7 +17,7 @@ from extras.views import ObjectConfigContextView, ObjectRenderConfigView
 from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
 from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
 from netbox.object_actions import *
 from netbox.object_actions import *
-from netbox.ui.components import NestedGroupObjectPanel
+from netbox.ui import layout
 from netbox.views import generic
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -228,7 +228,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
         regions = instance.get_descendants(include_self=True)
         regions = instance.get_descendants(include_self=True)
 
 
         return {
         return {
-            'region_panel': NestedGroupObjectPanel(instance, _('Region')),
+            'region_panel': panels.NestedGroupObjectPanel(instance, _('Region')),
             'related_models': self.get_related_models(
             'related_models': self.get_related_models(
                 request,
                 request,
                 regions,
                 regions,
@@ -340,7 +340,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
         groups = instance.get_descendants(include_self=True)
         groups = instance.get_descendants(include_self=True)
 
 
         return {
         return {
-            'sitegroup_panel': NestedGroupObjectPanel(instance, _('Site Group')),
+            'sitegroup_panel': panels.NestedGroupObjectPanel(instance, _('Site Group')),
             'related_models': self.get_related_models(
             'related_models': self.get_related_models(
                 request,
                 request,
                 groups,
                 groups,
@@ -465,10 +465,17 @@ class SiteListView(generic.ObjectListView):
 @register_model_view(Site)
 @register_model_view(Site)
 class SiteView(GetRelatedModelsMixin, generic.ObjectView):
 class SiteView(GetRelatedModelsMixin, generic.ObjectView):
     queryset = Site.objects.prefetch_related('tenant__group')
     queryset = Site.objects.prefetch_related('tenant__group')
+    layout = layout.Layout(
+        layout.Row(
+            layout.Column(
+                panels.SitePanel(_('Site'))
+            ),
+        )
+    )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         return {
         return {
-            'site_panel': panels.SitePanel(instance, _('Site')),
+            # 'site_panel': panels.SitePanel(instance, _('Site')),
             'related_models': self.get_related_models(
             'related_models': self.get_related_models(
                 request,
                 request,
                 instance,
                 instance,

+ 8 - 8
netbox/netbox/ui/attrs.py

@@ -35,7 +35,7 @@ class Attr(ABC):
 
 
 
 
 class TextAttr(Attr):
 class TextAttr(Attr):
-    template_name = 'components/attrs/text.html'
+    template_name = 'ui/attrs/text.html'
 
 
     def __init__(self, *args, style=None, copy_button=False, **kwargs):
     def __init__(self, *args, style=None, copy_button=False, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -56,7 +56,7 @@ class TextAttr(Attr):
 
 
 
 
 class ChoiceAttr(Attr):
 class ChoiceAttr(Attr):
-    template_name = 'components/attrs/choice.html'
+    template_name = 'ui/attrs/choice.html'
 
 
     def render(self, obj, context=None):
     def render(self, obj, context=None):
         context = context or {}
         context = context or {}
@@ -78,7 +78,7 @@ class ChoiceAttr(Attr):
 
 
 
 
 class ObjectAttr(Attr):
 class ObjectAttr(Attr):
-    template_name = 'components/attrs/object.html'
+    template_name = 'ui/attrs/object.html'
 
 
     def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
     def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -101,7 +101,7 @@ class ObjectAttr(Attr):
 
 
 
 
 class NestedObjectAttr(Attr):
 class NestedObjectAttr(Attr):
-    template_name = 'components/attrs/nested_object.html'
+    template_name = 'ui/attrs/nested_object.html'
 
 
     def __init__(self, *args, linkify=None, max_depth=None, **kwargs):
     def __init__(self, *args, linkify=None, max_depth=None, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -124,7 +124,7 @@ class NestedObjectAttr(Attr):
 
 
 
 
 class AddressAttr(Attr):
 class AddressAttr(Attr):
-    template_name = 'components/attrs/address.html'
+    template_name = 'ui/attrs/address.html'
 
 
     def __init__(self, *args, map_url=True, **kwargs):
     def __init__(self, *args, map_url=True, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
@@ -148,7 +148,7 @@ class AddressAttr(Attr):
 
 
 
 
 class GPSCoordinatesAttr(Attr):
 class GPSCoordinatesAttr(Attr):
-    template_name = 'components/attrs/gps_coordinates.html'
+    template_name = 'ui/attrs/gps_coordinates.html'
 
 
     def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
     def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
         kwargs.setdefault('label', _('GPS Coordinates'))
         kwargs.setdefault('label', _('GPS Coordinates'))
@@ -177,7 +177,7 @@ class GPSCoordinatesAttr(Attr):
 
 
 
 
 class TimezoneAttr(Attr):
 class TimezoneAttr(Attr):
-    template_name = 'components/attrs/timezone.html'
+    template_name = 'ui/attrs/timezone.html'
 
 
     def render(self, obj, context=None):
     def render(self, obj, context=None):
         context = context or {}
         context = context or {}
@@ -213,7 +213,7 @@ class TemplatedAttr(Attr):
 
 
 
 
 class UtilizationAttr(Attr):
 class UtilizationAttr(Attr):
-    template_name = 'components/attrs/utilization.html'
+    template_name = 'ui/attrs/utilization.html'
 
 
     def render(self, obj, context=None):
     def render(self, obj, context=None):
         context = context or {}
         context = context or {}

+ 44 - 0
netbox/netbox/ui/layout.py

@@ -0,0 +1,44 @@
+from netbox.ui.panels import Panel
+
+__all__ = (
+    'Column',
+    'Layout',
+    'Row',
+)
+
+
+class Layout:
+
+    def __init__(self, *rows):
+        for i, row in enumerate(rows):
+            if type(row) is not Row:
+                raise TypeError(f"Row {i} must be a Row instance, not {type(row)}.")
+        self.rows = rows
+
+    def render(self, context):
+        return ''.join([row.render(context) for row in self.rows])
+
+
+class Row:
+    template_name = 'ui/layout/row.html'
+
+    def __init__(self, *columns):
+        for i, column in enumerate(columns):
+            if type(column) is not Column:
+                raise TypeError(f"Column {i} must be a Column instance, not {type(column)}.")
+        self.columns = columns
+
+    def render(self, context):
+        return ''.join([column.render(context) for column in self.columns])
+
+
+class Column:
+
+    def __init__(self, *panels):
+        for i, panel in enumerate(panels):
+            if not isinstance(panel, Panel):
+                raise TypeError(f"Panel {i} must be an instance of a Panel, not {type(panel)}.")
+        self.panels = panels
+
+    def render(self, context):
+        return ''.join([panel.render(context) for panel in self.panels])

+ 19 - 21
netbox/netbox/ui/components.py → netbox/netbox/ui/panels.py

@@ -1,5 +1,4 @@
 from abc import ABC, ABCMeta, abstractmethod
 from abc import ABC, ABCMeta, abstractmethod
-from functools import cached_property
 
 
 from django.template.loader import render_to_string
 from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
@@ -8,18 +7,21 @@ from netbox.ui import attrs
 from netbox.ui.attrs import Attr
 from netbox.ui.attrs import Attr
 from utilities.string import title
 from utilities.string import title
 
 
+__all__ = (
+    'NestedGroupObjectPanel',
+    'ObjectPanel',
+    'Panel',
+)
 
 
-class Component(ABC):
+
+class Panel(ABC):
 
 
     @abstractmethod
     @abstractmethod
-    def render(self):
+    def render(self, obj):
         pass
         pass
 
 
-    def __str__(self):
-        return self.render()
-
 
 
-class ObjectDetailsPanelMeta(ABCMeta):
+class ObjectPanelMeta(ABCMeta):
 
 
     def __new__(mcls, name, bases, namespace, **kwargs):
     def __new__(mcls, name, bases, namespace, **kwargs):
         declared = {}
         declared = {}
@@ -46,33 +48,29 @@ class ObjectDetailsPanelMeta(ABCMeta):
         return cls
         return cls
 
 
 
 
-class ObjectPanel(Component, metaclass=ObjectDetailsPanelMeta):
-    template_name = 'components/object_details_panel.html'
+class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
+    template_name = 'ui/panels/object.html'
 
 
-    def __init__(self, obj, title=None):
-        self.object = obj
-        self.title = title or obj._meta.verbose_name
+    def __init__(self, title=None):
+        self.title = title
 
 
-    @cached_property
-    def attributes(self):
+    def get_attributes(self, obj):
         return [
         return [
             {
             {
                 'label': attr.label or title(name),
                 'label': attr.label or title(name),
-                'value': attr.render(self.object, {'name': name}),
+                'value': attr.render(obj, {'name': name}),
             } for name, attr in self._attrs.items()
             } for name, attr in self._attrs.items()
         ]
         ]
 
 
-    def render(self):
+    def render(self, context):
+        obj = context.get('object')
         return render_to_string(self.template_name, {
         return render_to_string(self.template_name, {
             'title': self.title,
             'title': self.title,
-            'attrs': self.attributes,
+            'attrs': self.get_attributes(obj),
         })
         })
 
 
-    def __str__(self):
-        return self.render()
-
 
 
-class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectDetailsPanelMeta):
+class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
     name = attrs.TextAttr('name', label=_('Name'))
     name = attrs.TextAttr('name', label=_('Name'))
     description = attrs.TextAttr('description', label=_('Description'))
     description = attrs.TextAttr('description', label=_('Description'))
     parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
     parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)

+ 5 - 0
netbox/netbox/views/generic/object_views.py

@@ -47,6 +47,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
         tab: A ViewTab instance for the view
         tab: A ViewTab instance for the view
         actions: An iterable of ObjectAction subclasses (see ActionsMixin)
         actions: An iterable of ObjectAction subclasses (see ActionsMixin)
     """
     """
+    layout = None
     tab = None
     tab = None
     actions = (CloneObject, EditObject, DeleteObject)
     actions = (CloneObject, EditObject, DeleteObject)
 
 
@@ -58,6 +59,9 @@ class ObjectView(ActionsMixin, BaseObjectView):
         Return self.template_name if defined. Otherwise, dynamically resolve the template name using the queryset
         Return self.template_name if defined. Otherwise, dynamically resolve the template name using the queryset
         model's `app_label` and `model_name`.
         model's `app_label` and `model_name`.
         """
         """
+        # TODO: Temporarily allow layout to override template_name
+        if self.layout is not None:
+            return 'generic/object.html'
         if self.template_name is not None:
         if self.template_name is not None:
             return self.template_name
             return self.template_name
         model_opts = self.queryset.model._meta
         model_opts = self.queryset.model._meta
@@ -81,6 +85,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
             'object': instance,
             'object': instance,
             'actions': actions,
             'actions': actions,
             'tab': self.tab,
             'tab': self.tab,
+            'layout': self.layout,
             **self.get_extra_context(request, instance),
             **self.get_extra_context(request, instance),
         })
         })
 
 

+ 14 - 1
netbox/templates/generic/object.html

@@ -122,7 +122,20 @@ Context:
   {% plugin_alerts object %}
   {% plugin_alerts object %}
 {% endblock alerts %}
 {% endblock alerts %}
 
 
-{% block content %}{% endblock %}
+{% block content %}
+  {# Render panel layout declared on view class #}
+  {% for row in layout.rows %}
+    <div class="row">
+      {% for column in row.columns %}
+        <div class="col">
+          {% for panel in column.panels %}
+            {% render_panel panel %}
+          {% endfor %}
+        </div>
+      {% endfor %}
+    </div>
+  {% endfor %}
+{% endblock %}
 
 
 {% block modals %}
 {% block modals %}
   {% include 'inc/htmx_modal.html' %}
   {% include 'inc/htmx_modal.html' %}

+ 0 - 0
netbox/templates/components/attrs/address.html → netbox/templates/ui/attrs/address.html


+ 0 - 0
netbox/templates/components/attrs/choice.html → netbox/templates/ui/attrs/choice.html


+ 0 - 0
netbox/templates/components/attrs/gps_coordinates.html → netbox/templates/ui/attrs/gps_coordinates.html


+ 0 - 0
netbox/templates/components/attrs/nested_object.html → netbox/templates/ui/attrs/nested_object.html


+ 0 - 0
netbox/templates/components/attrs/object.html → netbox/templates/ui/attrs/object.html


+ 0 - 0
netbox/templates/components/attrs/text.html → netbox/templates/ui/attrs/text.html


+ 0 - 0
netbox/templates/components/attrs/timezone.html → netbox/templates/ui/attrs/timezone.html


+ 0 - 0
netbox/templates/components/attrs/utilization.html → netbox/templates/ui/attrs/utilization.html


+ 0 - 0
netbox/templates/components/object_details_panel.html → netbox/templates/ui/panels/object.html


+ 6 - 0
netbox/utilities/templatetags/builtins/tags.py

@@ -3,6 +3,7 @@ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
 
 
 from django import template
 from django import template
 from django.templatetags.static import static
 from django.templatetags.static import static
+from django.utils.safestring import mark_safe
 
 
 from extras.choices import CustomFieldTypeChoices
 from extras.choices import CustomFieldTypeChoices
 from utilities.querydict import dict_to_querydict
 from utilities.querydict import dict_to_querydict
@@ -179,3 +180,8 @@ def static_with_params(path, **params):
     # Reconstruct the URL with the new query string
     # Reconstruct the URL with the new query string
     new_parsed = parsed._replace(query=new_query)
     new_parsed = parsed._replace(query=new_query)
     return urlunparse(new_parsed)
     return urlunparse(new_parsed)
+
+
+@register.simple_tag(takes_context=True)
+def render_panel(context, panel):
+    return mark_safe(panel.render(context))