mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-14 04:19:36 -06:00
Implement layout declaration under view
This commit is contained in:
parent
eef9db5e5a
commit
3fd4664a76
@ -1,9 +1,9 @@
|
||||
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)
|
||||
group = attrs.NestedObjectAttr('group', label=_('Group'), linkify=True)
|
||||
status = attrs.ChoiceAttr('status', label=_('Status'))
|
||||
@ -16,14 +16,14 @@ class SitePanel(components.ObjectPanel):
|
||||
gps_coordinates = attrs.GPSCoordinatesAttr()
|
||||
|
||||
|
||||
class LocationPanel(components.NestedGroupObjectPanel):
|
||||
class LocationPanel(panels.NestedGroupObjectPanel):
|
||||
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
|
||||
status = attrs.ChoiceAttr('status', label=_('Status'))
|
||||
tenant = attrs.ObjectAttr('tenant', label=_('Tenant'), linkify=True, grouped_by='group')
|
||||
facility = attrs.TextAttr('facility', label=_('Facility'))
|
||||
|
||||
|
||||
class RackPanel(components.ObjectPanel):
|
||||
class RackPanel(panels.ObjectPanel):
|
||||
region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
|
||||
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
|
||||
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'))
|
||||
|
||||
|
||||
class DevicePanel(components.ObjectPanel):
|
||||
class DevicePanel(panels.ObjectPanel):
|
||||
region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
|
||||
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
|
||||
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)
|
||||
|
||||
|
||||
class DeviceManagementPanel(components.ObjectPanel):
|
||||
class DeviceManagementPanel(panels.ObjectPanel):
|
||||
status = attrs.ChoiceAttr('status', label=_('Status'))
|
||||
role = attrs.NestedObjectAttr('role', label=_('Role'), linkify=True, max_depth=3)
|
||||
platform = attrs.NestedObjectAttr('platform', label=_('Platform'), linkify=True, max_depth=3)
|
||||
|
||||
@ -17,7 +17,7 @@ from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.object_actions import *
|
||||
from netbox.ui.components import NestedGroupObjectPanel
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
@ -228,7 +228,7 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
regions = instance.get_descendants(include_self=True)
|
||||
|
||||
return {
|
||||
'region_panel': NestedGroupObjectPanel(instance, _('Region')),
|
||||
'region_panel': panels.NestedGroupObjectPanel(instance, _('Region')),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
regions,
|
||||
@ -340,7 +340,7 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
|
||||
return {
|
||||
'sitegroup_panel': NestedGroupObjectPanel(instance, _('Site Group')),
|
||||
'sitegroup_panel': panels.NestedGroupObjectPanel(instance, _('Site Group')),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
groups,
|
||||
@ -465,10 +465,17 @@ class SiteListView(generic.ObjectListView):
|
||||
@register_model_view(Site)
|
||||
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Site.objects.prefetch_related('tenant__group')
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
panels.SitePanel(_('Site'))
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'site_panel': panels.SitePanel(instance, _('Site')),
|
||||
# 'site_panel': panels.SitePanel(instance, _('Site')),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
|
||||
@ -35,7 +35,7 @@ class Attr(ABC):
|
||||
|
||||
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -56,7 +56,7 @@ class TextAttr(Attr):
|
||||
|
||||
|
||||
class ChoiceAttr(Attr):
|
||||
template_name = 'components/attrs/choice.html'
|
||||
template_name = 'ui/attrs/choice.html'
|
||||
|
||||
def render(self, obj, context=None):
|
||||
context = context or {}
|
||||
@ -78,7 +78,7 @@ class ChoiceAttr(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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -101,7 +101,7 @@ class ObjectAttr(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):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -124,7 +124,7 @@ class NestedObjectAttr(Attr):
|
||||
|
||||
|
||||
class AddressAttr(Attr):
|
||||
template_name = 'components/attrs/address.html'
|
||||
template_name = 'ui/attrs/address.html'
|
||||
|
||||
def __init__(self, *args, map_url=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -148,7 +148,7 @@ class AddressAttr(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):
|
||||
kwargs.setdefault('label', _('GPS Coordinates'))
|
||||
@ -177,7 +177,7 @@ class GPSCoordinatesAttr(Attr):
|
||||
|
||||
|
||||
class TimezoneAttr(Attr):
|
||||
template_name = 'components/attrs/timezone.html'
|
||||
template_name = 'ui/attrs/timezone.html'
|
||||
|
||||
def render(self, obj, context=None):
|
||||
context = context or {}
|
||||
@ -213,7 +213,7 @@ class TemplatedAttr(Attr):
|
||||
|
||||
|
||||
class UtilizationAttr(Attr):
|
||||
template_name = 'components/attrs/utilization.html'
|
||||
template_name = 'ui/attrs/utilization.html'
|
||||
|
||||
def render(self, obj, context=None):
|
||||
context = context or {}
|
||||
|
||||
44
netbox/netbox/ui/layout.py
Normal file
44
netbox/netbox/ui/layout.py
Normal file
@ -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])
|
||||
@ -1,5 +1,4 @@
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from functools import cached_property
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -8,18 +7,21 @@ from netbox.ui import attrs
|
||||
from netbox.ui.attrs import Attr
|
||||
from utilities.string import title
|
||||
|
||||
__all__ = (
|
||||
'NestedGroupObjectPanel',
|
||||
'ObjectPanel',
|
||||
'Panel',
|
||||
)
|
||||
|
||||
class Component(ABC):
|
||||
|
||||
class Panel(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def render(self):
|
||||
def render(self, obj):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.render()
|
||||
|
||||
|
||||
class ObjectDetailsPanelMeta(ABCMeta):
|
||||
class ObjectPanelMeta(ABCMeta):
|
||||
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
declared = {}
|
||||
@ -46,33 +48,29 @@ class ObjectDetailsPanelMeta(ABCMeta):
|
||||
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 [
|
||||
{
|
||||
'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()
|
||||
]
|
||||
|
||||
def render(self):
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return render_to_string(self.template_name, {
|
||||
'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'))
|
||||
description = attrs.TextAttr('description', label=_('Description'))
|
||||
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
|
||||
@ -47,6 +47,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
||||
tab: A ViewTab instance for the view
|
||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||
"""
|
||||
layout = None
|
||||
tab = None
|
||||
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
|
||||
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:
|
||||
return self.template_name
|
||||
model_opts = self.queryset.model._meta
|
||||
@ -81,6 +85,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
||||
'object': instance,
|
||||
'actions': actions,
|
||||
'tab': self.tab,
|
||||
'layout': self.layout,
|
||||
**self.get_extra_context(request, instance),
|
||||
})
|
||||
|
||||
|
||||
@ -122,7 +122,20 @@ Context:
|
||||
{% plugin_alerts object %}
|
||||
{% 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 %}
|
||||
{% include 'inc/htmx_modal.html' %}
|
||||
|
||||
@ -3,6 +3,7 @@ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||
|
||||
from django import template
|
||||
from django.templatetags.static import static
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
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
|
||||
new_parsed = parsed._replace(query=new_query)
|
||||
return urlunparse(new_parsed)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def render_panel(context, panel):
|
||||
return mark_safe(panel.render(context))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user