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

+ 23 - 1
netbox/dcim/views.py

@@ -16,6 +16,9 @@ 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.templates.components import (
+    AttributesPanel, EmbeddedTemplate, GPSCoordinatesAttr, NestedObjectAttr, ObjectAttr, TextAttr,
+)
 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
@@ -2223,9 +2226,28 @@ class DeviceView(generic.ObjectView):
         else:
         else:
             vc_members = []
             vc_members = []
 
 
+        device_attrs = AttributesPanel(_('Device'), {
+            _('Region'): NestedObjectAttr(instance.site.region, linkify=True),
+            _('Site'): ObjectAttr(instance.site, linkify=True, grouped_by='group'),
+            _('Location'): ObjectAttr(instance.location, linkify=True),
+            # TODO: Include position & face of parent device (if applicable)
+            _('Rack'): EmbeddedTemplate('dcim/device/attrs/rack.html', {'device': instance}),
+            _('Virtual Chassis'): ObjectAttr(instance.virtual_chassis, linkify=True),
+            _('Parent Device'): EmbeddedTemplate('dcim/device/attrs/parent_device.html', {'device': instance}),
+            _('GPS Coordinates'): GPSCoordinatesAttr(instance.latitude, instance.longitude),
+            _('Tenant'): ObjectAttr(instance.tenant, linkify=True, grouped_by='group'),
+            _('Device Type'): ObjectAttr(instance.device_type, linkify=True, grouped_by='manufacturer'),
+            _('Description'): TextAttr(instance.description),
+            _('Airflow'): TextAttr(instance.get_airflow_display()),
+            _('Serial Number'): TextAttr(instance.serial, style='font-monospace'),
+            _('Asset Tag'): TextAttr(instance.asset_tag, style='font-monospace'),
+            _('Config Template'): ObjectAttr(instance.config_template, linkify=True),
+        })
+
         return {
         return {
             'vc_members': vc_members,
             'vc_members': vc_members,
-            'svg_extra': f'highlight=id:{instance.pk}'
+            'svg_extra': f'highlight=id:{instance.pk}',
+            'device_attrs': device_attrs,
         }
         }
 
 
 
 

+ 0 - 0
netbox/netbox/templates/__init__.py


+ 139 - 0
netbox/netbox/templates/components.py

@@ -0,0 +1,139 @@
+from abc import ABC, abstractmethod
+
+from django.template.loader import render_to_string
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+
+from netbox.config import get_config
+
+
+class Component(ABC):
+
+    @abstractmethod
+    def render(self):
+        pass
+
+    def __str__(self):
+        return self.render()
+
+
+#
+# Attributes
+#
+
+class Attr(Component):
+    template_name = None
+    placeholder = mark_safe('<span class="text-muted">&mdash;</span>')
+
+
+class TextAttr(Attr):
+
+    def __init__(self, value, style=None):
+        self.value = value
+        self.style = style
+
+    def render(self):
+        if self.value in (None, ''):
+            return self.placeholder
+        if self.style:
+            return mark_safe(f'<span class="{self.style}">{escape(self.value)}</span>')
+        return self.value
+
+
+class ObjectAttr(Attr):
+    template_name = 'components/object.html'
+
+    def __init__(self, obj, linkify=None, grouped_by=None, template_name=None):
+        self.object = obj
+        self.linkify = linkify
+        self.group = getattr(obj, grouped_by, None) if grouped_by else None
+        self.template_name = template_name or self.template_name
+
+    def render(self):
+        if self.object is None:
+            return self.placeholder
+
+        # Determine object & group URLs
+        # TODO: Add support for reverse() lookups
+        if self.linkify and hasattr(self.object, 'get_absolute_url'):
+            object_url = self.object.get_absolute_url()
+        else:
+            object_url = None
+        if self.linkify and hasattr(self.group, 'get_absolute_url'):
+            group_url = self.group.get_absolute_url()
+        else:
+            group_url = None
+
+        return render_to_string(self.template_name, {
+            'object': self.object,
+            'object_url': object_url,
+            'group': self.group,
+            'group_url': group_url,
+        })
+
+
+class NestedObjectAttr(Attr):
+    template_name = 'components/nested_object.html'
+
+    def __init__(self, obj, linkify=None):
+        self.object = obj
+        self.linkify = linkify
+
+    def render(self):
+        if not self.object:
+            return self.placeholder
+        return render_to_string(self.template_name, {
+            'nodes': self.object.get_ancestors(include_self=True),
+            'linkify': self.linkify,
+        })
+
+
+class GPSCoordinatesAttr(Attr):
+    template_name = 'components/gps_coordinates.html'
+
+    def __init__(self, latitude, longitude, map_url=True):
+        self.latitude = latitude
+        self.longitude = longitude
+        if map_url is True:
+            self.map_url = get_config().MAPS_URL
+        elif map_url:
+            self.map_url = map_url
+        else:
+            self.map_url = None
+
+    def render(self):
+        if not (self.latitude and self.longitude):
+            return self.placeholder
+        return render_to_string(self.template_name, {
+            'latitude': self.latitude,
+            'longitude': self.longitude,
+            'map_url': self.map_url,
+        })
+
+
+#
+# Components
+#
+
+class AttributesPanel(Component):
+    template_name = 'components/attributes_panel.html'
+
+    def __init__(self, title, attrs):
+        self.title = title
+        self.attrs = attrs
+
+    def render(self):
+        return render_to_string(self.template_name, {
+            'title': self.title,
+            'attrs': self.attrs,
+        })
+
+
+class EmbeddedTemplate(Component):
+
+    def __init__(self, template_name, context=None):
+        self.template_name = template_name
+        self.context = context or {}
+
+    def render(self):
+        return render_to_string(self.template_name, self.context)

+ 13 - 0
netbox/templates/components/attributes_panel.html

@@ -0,0 +1,13 @@
+<div class="card">
+  <h2 class="card-header">{{ title }}</h2>
+  <table class="table table-hover attr-table">
+    {% for label, attr in attrs.items %}
+      <tr>
+        <th scope="row">{{ label }}</th>
+        <td>
+          <div class="d-flex justify-content-between align-items-start">{{ attr }}</div>
+        </td>
+      </tr>
+    {% endfor %}
+  </table>
+</div>

+ 8 - 0
netbox/templates/components/gps_coordinates.html

@@ -0,0 +1,8 @@
+{% load i18n %}
+{% load l10n %}
+<span>{{ latitude }}, {{ longitude }}</span>
+{% if map_url %}
+  <a href="{{ map_url }}{{ latitude|unlocalize }},{{ longitude|unlocalize }}" target="_blank" class="btn btn-primary btn-sm print-none">
+    <i class="mdi mdi-map-marker"></i> {% trans "Map" %}
+  </a>
+{% endif %}

+ 11 - 0
netbox/templates/components/nested_object.html

@@ -0,0 +1,11 @@
+<ol class="breadcrumb" aria-label="breadcrumbs">
+  {% for node in nodes %}
+    <li class="breadcrumb-item">
+      {% if linkify %}
+        <a href="{{ node.get_absolute_url }}">{{ node }}</a>
+      {% else %}
+        {{ node }}
+      {% endif %}
+    </li>
+  {% endfor %}
+</ol>

+ 26 - 0
netbox/templates/components/object.html

@@ -0,0 +1,26 @@
+{% if group %}
+  {# Display an object with its parent group #}
+  <ol class="breadcrumb" aria-label="breadcrumbs">
+    <li class="breadcrumb-item">
+      {% if group_url %}
+        <a href="{{ group_url }}">{{ group }}</a>
+      {% else %}
+        {{ object.group }}
+      {% endif %}
+    </li>
+    <li class="breadcrumb-item">
+      {% if object_url %}
+        <a href="{{ object_url }}">{{ object }}</a>
+      {% else %}
+        {{ object }}
+      {% endif %}
+    </li>
+  </ol>
+{% else %}
+  {# Display only the object #}
+  {% if object_url %}
+    <a href="{{ object_url }}">{{ object }}</a>
+  {% else %}
+    {{ object }}
+  {% endif %}
+{% endif %}

+ 1 - 0
netbox/templates/dcim/device.html

@@ -177,6 +177,7 @@
             {% plugin_left_page object %}
             {% plugin_left_page object %}
         </div>
         </div>
         <div class="col col-12 col-xl-6">
         <div class="col col-12 col-xl-6">
+            {{ device_attrs }}
             <div class="card">
             <div class="card">
                 <h2 class="card-header">{% trans "Management" %}</h2>
                 <h2 class="card-header">{% trans "Management" %}</h2>
                 <table class="table table-hover attr-table">
                 <table class="table table-hover attr-table">

+ 8 - 0
netbox/templates/dcim/device/attrs/parent_device.html

@@ -0,0 +1,8 @@
+{% if device.parent_bay %}
+  <ol class="breadcrumb" aria-label="breadcrumbs">
+    <li class="breadcrumb-item">{{ device.parent_bay.device|linkify }}</li>
+    <li class="breadcrumb-item">{{ device.parent_bay }}</li>
+  </ol>
+{% else %}
+  {{ ''|placeholder }}
+{% endif %}

+ 18 - 0
netbox/templates/dcim/device/attrs/rack.html

@@ -0,0 +1,18 @@
+{% load i18n %}
+{% if device.rack %}
+  <span>
+    {{ device.rack|linkify }}
+    {% if device.rack and device.position %}
+      (U{{ device.position|floatformat }} / {{ device.get_face_display }})
+    {% elif device.rack and device.device_type.u_height %}
+      <span class="badge text-bg-warning">{% trans "Not racked" %}</span>
+    {% endif %}
+  </span>
+  {% if device.rack and device.position %}
+    <a href="{{ device.rack.get_absolute_url }}?device={{ device.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
+      <i class="mdi mdi-view-day-outline"></i>
+    </a>
+  {% endif %}
+{% else %}
+  {{ ''|placeholder }}
+{% endif %}