mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-15 12:59:35 -06:00
Merge pull request #20737 from netbox-community/20204-template-components
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Closes #20204: Introduce modular template components
This commit is contained in:
commit
1d2f6a82cb
148
docs/plugins/development/ui-components.md
Normal file
148
docs/plugins/development/ui-components.md
Normal file
@ -0,0 +1,148 @@
|
||||
# UI Components
|
||||
|
||||
!!! note "New in NetBox v4.5"
|
||||
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
|
||||
|
||||
!!! danger "Beta Feature"
|
||||
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
|
||||
|
||||
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
||||
|
||||
## Page Layout
|
||||
|
||||
A layout defines the general arrangement of content on a page into rows and columns. The layout is defined under the [view](./views.md) and declares a set of rows, each of which may have one or more columns. Below is an example layout.
|
||||
|
||||
```
|
||||
+-------+-------+-------+
|
||||
| Col 1 | Col 2 | Col 3 |
|
||||
+-------+-------+-------+
|
||||
| Col 4 |
|
||||
+-----------+-----------+
|
||||
| Col 5 | Col 6 |
|
||||
+-----------+-----------+
|
||||
```
|
||||
|
||||
The above layout can be achieved with the following declaration under a view:
|
||||
|
||||
```python
|
||||
from netbox.ui import layout
|
||||
from netbox.views import generic
|
||||
|
||||
class MyView(generic.ObjectView):
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(),
|
||||
layout.Column(),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
Currently, layouts are supported only for subclasses of [`generic.ObjectView`](./views.md#netbox.views.generic.ObjectView).
|
||||
|
||||
::: netbox.ui.layout.Layout
|
||||
|
||||
::: netbox.ui.layout.SimpleLayout
|
||||
|
||||
::: netbox.ui.layout.Row
|
||||
|
||||
::: netbox.ui.layout.Column
|
||||
|
||||
## Panels
|
||||
|
||||
Within each column, related blocks of content are arranged into panels. Each panel has a title and may have a set of associated actions, but the content within is otherwise arbitrary.
|
||||
|
||||
Plugins can define their own panels by inheriting from the base class `netbox.ui.panels.Panel`. Override the `get_context()` method to pass additional context to your custom panel template. An example is provided below.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.ui.panels import Panel
|
||||
|
||||
class RecentChangesPanel(Panel):
|
||||
template_name = 'my_plugin/panels/recent_changes.html'
|
||||
title = _('Recent Changes')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'changes': get_changes()[:10],
|
||||
}
|
||||
```
|
||||
|
||||
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
|
||||
|
||||
::: netbox.ui.panels.Panel
|
||||
|
||||
::: netbox.ui.panels.ObjectPanel
|
||||
|
||||
::: netbox.ui.panels.ObjectAttributesPanel
|
||||
|
||||
#### Object Attributes
|
||||
|
||||
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
||||
|
||||
| Class | Description |
|
||||
|--------------------------------------|--------------------------------------------------|
|
||||
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
||||
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
||||
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
||||
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
||||
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
||||
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
||||
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
|
||||
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
||||
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
||||
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
||||
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
||||
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
||||
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
||||
|
||||
::: netbox.ui.panels.OrganizationalObjectPanel
|
||||
|
||||
::: netbox.ui.panels.NestedGroupObjectPanel
|
||||
|
||||
::: netbox.ui.panels.CommentsPanel
|
||||
|
||||
::: netbox.ui.panels.JSONPanel
|
||||
|
||||
::: netbox.ui.panels.RelatedObjectsPanel
|
||||
|
||||
::: netbox.ui.panels.ObjectsTablePanel
|
||||
|
||||
::: netbox.ui.panels.TemplatePanel
|
||||
|
||||
::: netbox.ui.panels.PluginContentPanel
|
||||
|
||||
## Panel Actions
|
||||
|
||||
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.ui import actions, panels
|
||||
|
||||
panels.ObjectsTablePanel(
|
||||
model='dcim.Region',
|
||||
title=_('Child Regions'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
::: netbox.ui.actions.PanelAction
|
||||
|
||||
::: netbox.ui.actions.LinkAction
|
||||
|
||||
::: netbox.ui.actions.AddObject
|
||||
|
||||
::: netbox.ui.actions.CopyContent
|
||||
@ -143,6 +143,7 @@ nav:
|
||||
- Getting Started: 'plugins/development/index.md'
|
||||
- Models: 'plugins/development/models.md'
|
||||
- Views: 'plugins/development/views.md'
|
||||
- UI Components: 'plugins/development/ui-components.md'
|
||||
- Navigation: 'plugins/development/navigation.md'
|
||||
- Templates: 'plugins/development/templates.md'
|
||||
- Tables: 'plugins/development/tables.md'
|
||||
|
||||
0
netbox/dcim/ui/__init__.py
Normal file
0
netbox/dcim/ui/__init__.py
Normal file
189
netbox/dcim/ui/panels.py
Normal file
189
netbox/dcim/ui/panels.py
Normal file
@ -0,0 +1,189 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs, panels
|
||||
|
||||
|
||||
class SitePanel(panels.ObjectAttributesPanel):
|
||||
region = attrs.NestedObjectAttr('region', linkify=True)
|
||||
group = attrs.NestedObjectAttr('group', linkify=True)
|
||||
name = attrs.TextAttr('name')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
facility = attrs.TextAttr('facility')
|
||||
description = attrs.TextAttr('description')
|
||||
timezone = attrs.TimezoneAttr('time_zone')
|
||||
physical_address = attrs.AddressAttr('physical_address', map_url=True)
|
||||
shipping_address = attrs.AddressAttr('shipping_address', map_url=True)
|
||||
gps_coordinates = attrs.GPSCoordinatesAttr()
|
||||
|
||||
|
||||
class LocationPanel(panels.NestedGroupObjectPanel):
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
facility = attrs.TextAttr('facility')
|
||||
|
||||
|
||||
class RackDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
form_factor = attrs.ChoiceAttr('form_factor')
|
||||
width = attrs.ChoiceAttr('width')
|
||||
height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
|
||||
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
|
||||
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
|
||||
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
|
||||
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
|
||||
|
||||
|
||||
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
||||
starting_unit = attrs.TextAttr('starting_unit')
|
||||
desc_units = attrs.BooleanAttr('desc_units', label=_('Descending units'))
|
||||
|
||||
|
||||
class RackPanel(panels.ObjectAttributesPanel):
|
||||
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||
location = attrs.NestedObjectAttr('location', linkify=True)
|
||||
name = attrs.TextAttr('name')
|
||||
facility = attrs.TextAttr('facility', label=_('Facility ID'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
rack_type = attrs.RelatedObjectAttr('rack_type', linkify=True, grouped_by='manufacturer')
|
||||
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
space_utilization = attrs.UtilizationAttr('get_utilization')
|
||||
power_utilization = attrs.UtilizationAttr('get_power_utilization')
|
||||
|
||||
|
||||
class RackWeightPanel(panels.ObjectAttributesPanel):
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight'))
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/rack/attrs/total_weight.html')
|
||||
|
||||
|
||||
class RackRolePanel(panels.OrganizationalObjectPanel):
|
||||
color = attrs.ColorAttr('color')
|
||||
|
||||
|
||||
class RackReservationPanel(panels.ObjectAttributesPanel):
|
||||
units = attrs.TextAttr('unit_list')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
user = attrs.RelatedObjectAttr('user')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class RackTypePanel(panels.ObjectAttributesPanel):
|
||||
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
|
||||
model = attrs.TextAttr('model')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class DevicePanel(panels.ObjectAttributesPanel):
|
||||
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||
location = attrs.NestedObjectAttr('location', linkify=True)
|
||||
rack = attrs.TemplatedAttr('rack', template_name='dcim/device/attrs/rack.html')
|
||||
virtual_chassis = attrs.RelatedObjectAttr('virtual_chassis', linkify=True)
|
||||
parent_device = attrs.TemplatedAttr('parent_bay', template_name='dcim/device/attrs/parent_device.html')
|
||||
gps_coordinates = attrs.GPSCoordinatesAttr()
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
device_type = attrs.RelatedObjectAttr('device_type', linkify=True, grouped_by='manufacturer')
|
||||
description = attrs.TextAttr('description')
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
|
||||
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
|
||||
|
||||
|
||||
class DeviceManagementPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Management')
|
||||
|
||||
status = attrs.ChoiceAttr('status')
|
||||
role = attrs.NestedObjectAttr('role', linkify=True, max_depth=3)
|
||||
platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
|
||||
primary_ip4 = attrs.TemplatedAttr(
|
||||
'primary_ip4',
|
||||
label=_('Primary IPv4'),
|
||||
template_name='dcim/device/attrs/ipaddress.html',
|
||||
)
|
||||
primary_ip6 = attrs.TemplatedAttr(
|
||||
'primary_ip6',
|
||||
label=_('Primary IPv6'),
|
||||
template_name='dcim/device/attrs/ipaddress.html',
|
||||
)
|
||||
oob_ip = attrs.TemplatedAttr(
|
||||
'oob_ip',
|
||||
label=_('Out-of-band IP'),
|
||||
template_name='dcim/device/attrs/ipaddress.html',
|
||||
)
|
||||
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
||||
|
||||
|
||||
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Dimensions')
|
||||
|
||||
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||
|
||||
|
||||
class DeviceTypePanel(panels.ObjectAttributesPanel):
|
||||
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
|
||||
model = attrs.TextAttr('model')
|
||||
part_number = attrs.TextAttr('part_number')
|
||||
default_platform = attrs.RelatedObjectAttr('default_platform', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
|
||||
exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization')
|
||||
full_depth = attrs.BooleanAttr('is_full_depth')
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
subdevice_role = attrs.ChoiceAttr('subdevice_role', label=_('Parent/child'))
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
front_image = attrs.ImageAttr('front_image')
|
||||
rear_image = attrs.ImageAttr('rear_image')
|
||||
|
||||
|
||||
class ModuleTypeProfilePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class VirtualChassisMembersPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel which lists all members of a virtual chassis.
|
||||
"""
|
||||
template_name = 'dcim/panels/virtual_chassis_members.html'
|
||||
title = _('Virtual Chassis Members')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'vc_members': context.get('vc_members'),
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
if not context.get('vc_members'):
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class PowerUtilizationPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel which displays the power utilization statistics for a device.
|
||||
"""
|
||||
template_name = 'dcim/panels/power_utilization.html'
|
||||
title = _('Power Utilization')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'vc_members': context.get('vc_members'),
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
obj = context['object']
|
||||
if not obj.powerports.exists() or not obj.poweroutlets.exists():
|
||||
return ''
|
||||
return super().render(context)
|
||||
@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
@ -12,10 +13,17 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.ui import panels
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
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 import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
@ -221,6 +229,27 @@ class RegionListView(generic.ObjectListView):
|
||||
@register_model_view(Region)
|
||||
class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Region.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
NestedGroupObjectPanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Region',
|
||||
title=_('Child Regions'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
regions = instance.get_descendants(include_self=True)
|
||||
@ -332,6 +361,27 @@ class SiteGroupListView(generic.ObjectListView):
|
||||
@register_model_view(SiteGroup)
|
||||
class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = SiteGroup.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
NestedGroupObjectPanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.SiteGroup',
|
||||
title=_('Child Groups'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Region', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
@ -461,6 +511,39 @@ class SiteListView(generic.ObjectListView):
|
||||
@register_model_view(Site)
|
||||
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Site.objects.prefetch_related('tenant__group')
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.SitePanel(),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Location',
|
||||
filters={'site_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Location', url_params={'site': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Device',
|
||||
title=_('Non-Racked Devices'),
|
||||
filters={
|
||||
'site_id': lambda ctx: ctx['object'].pk,
|
||||
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Device', url_params={'site': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@ -561,6 +644,52 @@ class LocationListView(generic.ObjectListView):
|
||||
@register_model_view(Location)
|
||||
class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Location.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.LocationPanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Location',
|
||||
title=_('Child Locations'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.Location',
|
||||
url_params={
|
||||
'site': lambda ctx: ctx['object'].site_id,
|
||||
'parent': lambda ctx: ctx['object'].pk,
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Device',
|
||||
title=_('Non-Racked Devices'),
|
||||
filters={
|
||||
'location_id': lambda ctx: ctx['object'].pk,
|
||||
'rack_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE,
|
||||
},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.Device',
|
||||
url_params={
|
||||
'site': lambda ctx: ctx['object'].site_id,
|
||||
'parent': lambda ctx: ctx['object'].pk,
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
locations = instance.get_descendants(include_self=True)
|
||||
@ -661,6 +790,16 @@ class RackRoleListView(generic.ObjectListView):
|
||||
@register_model_view(RackRole)
|
||||
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = RackRole.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RackRolePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@ -727,7 +866,22 @@ class RackTypeListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(RackType)
|
||||
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
template_name = 'generic/object.html'
|
||||
queryset = RackType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RackTypePanel(),
|
||||
panels.RackDimensionsPanel(title=_('Dimensions')),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.RackNumberingPanel(title=_('Numbering')),
|
||||
panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']),
|
||||
CustomFieldsPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@ -845,6 +999,22 @@ class RackElevationListView(generic.ObjectListView):
|
||||
@register_model_view(Rack)
|
||||
class RackView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RackPanel(),
|
||||
panels.RackDimensionsPanel(title=_('Dimensions')),
|
||||
panels.RackNumberingPanel(title=_('Numbering')),
|
||||
panels.RackWeightPanel(title=_('Weight')),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel('dcim/panels/rack_elevations.html'),
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||
@ -976,6 +1146,19 @@ class RackReservationListView(generic.ObjectListView):
|
||||
@register_model_view(RackReservation)
|
||||
class RackReservationView(generic.ObjectView):
|
||||
queryset = RackReservation.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RackPanel(accessor='object.rack', only=['region', 'site', 'location', 'name']),
|
||||
panels.RackReservationPanel(title=_('Reservation')),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel(template_name='dcim/panels/rack_reservation_elevations.html'),
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(RackReservation, 'add', detail=False)
|
||||
@ -1049,6 +1232,10 @@ class ManufacturerListView(generic.ObjectListView):
|
||||
@register_model_view(Manufacturer)
|
||||
class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[OrganizationalObjectPanel(), TagsPanel()],
|
||||
right_panels=[RelatedObjectsPanel(), CustomFieldsPanel()],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@ -1122,6 +1309,18 @@ class DeviceTypeListView(generic.ObjectListView):
|
||||
@register_model_view(DeviceType)
|
||||
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DeviceType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.DeviceTypePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@ -1372,7 +1571,36 @@ class ModuleTypeProfileListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(ModuleTypeProfile)
|
||||
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
template_name = 'generic/object.html'
|
||||
queryset = ModuleTypeProfile.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ModuleTypeProfilePanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
JSONPanel(field_name='schema', title=_('Schema')),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.ModuleType',
|
||||
title=_('Module Types'),
|
||||
filters={
|
||||
'profile_id': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'dcim.ModuleType',
|
||||
url_params={
|
||||
'profile': lambda ctx: ctx['object'].pk,
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(ModuleTypeProfile, 'add', detail=False)
|
||||
@ -2213,6 +2441,43 @@ class DeviceListView(generic.ObjectListView):
|
||||
@register_model_view(Device)
|
||||
class DeviceView(generic.ObjectView):
|
||||
queryset = Device.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.DevicePanel(),
|
||||
panels.VirtualChassisMembersPanel(),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
ObjectsTablePanel(
|
||||
model='dcim.VirtualDeviceContext',
|
||||
filters={'device_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.VirtualDeviceContext', url_params={'device': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
],
|
||||
right_panels=[
|
||||
panels.DeviceManagementPanel(),
|
||||
panels.PowerUtilizationPanel(),
|
||||
ObjectsTablePanel(
|
||||
model='ipam.Service',
|
||||
title=_('Application Services'),
|
||||
filters={'device_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.Service',
|
||||
url_params={
|
||||
'parent_object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||
'parent': lambda ctx: ctx['object'].pk
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
ImageAttachmentsPanel(),
|
||||
panels.DeviceDimensionsPanel(),
|
||||
TemplatePanel('dcim/panels/device_rack_elevations.html'),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# VirtualChassis members
|
||||
@ -2225,7 +2490,7 @@ class DeviceView(generic.ObjectView):
|
||||
|
||||
return {
|
||||
'vc_members': vc_members,
|
||||
'svg_extra': f'highlight=id:{instance.pk}'
|
||||
'svg_extra': f'highlight=id:{instance.pk}',
|
||||
}
|
||||
|
||||
|
||||
|
||||
68
netbox/extras/ui/panels.py
Normal file
68
netbox/extras/ui/panels.py
Normal file
@ -0,0 +1,68 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, panels
|
||||
from utilities.data import resolve_attr_path
|
||||
|
||||
__all__ = (
|
||||
'CustomFieldsPanel',
|
||||
'ImageAttachmentsPanel',
|
||||
'TagsPanel',
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldsPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel showing the value of all custom fields defined on an object.
|
||||
"""
|
||||
template_name = 'extras/panels/custom_fields.html'
|
||||
title = _('Custom Fields')
|
||||
|
||||
def get_context(self, context):
|
||||
obj = resolve_attr_path(context, self.accessor)
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'custom_fields': obj.get_custom_fields_by_group(),
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
ctx = self.get_context(context)
|
||||
# Hide the panel if no custom fields exist
|
||||
if not ctx['custom_fields']:
|
||||
return ''
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
|
||||
|
||||
class ImageAttachmentsPanel(panels.ObjectsTablePanel):
|
||||
"""
|
||||
A panel showing all images attached to the object.
|
||||
"""
|
||||
actions = [
|
||||
actions.AddObject(
|
||||
'extras.imageattachment',
|
||||
url_params={
|
||||
'object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||
'object_id': lambda ctx: ctx['object'].pk,
|
||||
'return_url': lambda ctx: ctx['object'].get_absolute_url(),
|
||||
},
|
||||
label=_('Attach an image'),
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__('extras.imageattachment', **kwargs)
|
||||
|
||||
|
||||
class TagsPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel showing the tags assigned to the object.
|
||||
"""
|
||||
template_name = 'extras/panels/tags.html'
|
||||
title = _('Tags')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'object': resolve_attr_path(context, self.accessor),
|
||||
}
|
||||
0
netbox/netbox/ui/__init__.py
Normal file
0
netbox/netbox/ui/__init__.py
Normal file
157
netbox/netbox/ui/actions.py
Normal file
157
netbox/netbox/ui/actions.py
Normal file
@ -0,0 +1,157 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.apps import apps
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.views import get_viewname
|
||||
|
||||
__all__ = (
|
||||
'AddObject',
|
||||
'CopyContent',
|
||||
'LinkAction',
|
||||
'PanelAction',
|
||||
)
|
||||
|
||||
|
||||
class PanelAction:
|
||||
"""
|
||||
A link (typically a button) within a panel to perform some associated action, such as adding an object.
|
||||
|
||||
Attributes:
|
||||
template_name (str): The name of the template to render
|
||||
|
||||
Parameters:
|
||||
label (str): The human-friendly button text
|
||||
permissions (list): An iterable of permissions required to display the action
|
||||
button_class (str): Bootstrap CSS class for the button
|
||||
button_icon (str): Name of the button's MDI icon
|
||||
"""
|
||||
template_name = None
|
||||
|
||||
def __init__(self, label, permissions=None, button_class='primary', button_icon=None):
|
||||
self.label = label
|
||||
self.permissions = permissions
|
||||
self.button_class = button_class
|
||||
self.button_icon = button_icon
|
||||
|
||||
def get_context(self, context):
|
||||
"""
|
||||
Return the template context used to render the action element.
|
||||
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
return {
|
||||
'label': self.label,
|
||||
'button_class': self.button_class,
|
||||
'button_icon': self.button_icon,
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render the action as HTML.
|
||||
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
# Enforce permissions
|
||||
user = context['request'].user
|
||||
if not user.has_perms(self.permissions):
|
||||
return ''
|
||||
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
|
||||
|
||||
class LinkAction(PanelAction):
|
||||
"""
|
||||
A hyperlink (typically a button) within a panel to perform some associated action, such as adding an object.
|
||||
|
||||
Parameters:
|
||||
view_name (str): Name of the view to which the action will link
|
||||
view_kwargs (dict): Additional keyword arguments to pass to `reverse()` when resolving the URL
|
||||
url_params (dict): A dictionary of arbitrary URL parameters to append to the action's URL. If the value of a key
|
||||
is a callable, it will be passed the current template context.
|
||||
"""
|
||||
template_name = 'ui/actions/link.html'
|
||||
|
||||
def __init__(self, view_name, view_kwargs=None, url_params=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.view_name = view_name
|
||||
self.view_kwargs = view_kwargs or {}
|
||||
self.url_params = url_params or {}
|
||||
|
||||
def get_url(self, context):
|
||||
"""
|
||||
Resolve the URL for the action from its view name and kwargs. Append any additional URL parameters.
|
||||
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
url = reverse(self.view_name, kwargs=self.view_kwargs)
|
||||
if self.url_params:
|
||||
# If the param value is callable, call it with the context and save the result.
|
||||
url_params = {
|
||||
k: v(context) if callable(v) else v for k, v in self.url_params.items()
|
||||
}
|
||||
# Set the return URL if not already set and an object is available.
|
||||
if 'return_url' not in url_params and 'object' in context:
|
||||
url_params['return_url'] = context['object'].get_absolute_url()
|
||||
url = f'{url}?{urlencode(url_params)}'
|
||||
return url
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'url': self.get_url(context),
|
||||
}
|
||||
|
||||
|
||||
class AddObject(LinkAction):
|
||||
"""
|
||||
An action to add a new object.
|
||||
|
||||
Parameters:
|
||||
model (str): The dotted label of the model to be added (e.g. "dcim.site")
|
||||
url_params (dict): A dictionary of arbitrary URL parameters to append to the resolved URL
|
||||
"""
|
||||
def __init__(self, model, url_params=None, **kwargs):
|
||||
# Resolve the model class from its app.name label
|
||||
try:
|
||||
app_label, model_name = model.split('.')
|
||||
model = apps.get_model(app_label, model_name)
|
||||
except (ValueError, LookupError):
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
view_name = get_viewname(model, 'add')
|
||||
|
||||
kwargs.setdefault('label', _('Add'))
|
||||
kwargs.setdefault('button_icon', 'plus-thick')
|
||||
kwargs.setdefault('permissions', [get_permission_for_model(model, 'add')])
|
||||
|
||||
super().__init__(view_name=view_name, url_params=url_params, **kwargs)
|
||||
|
||||
|
||||
class CopyContent(PanelAction):
|
||||
"""
|
||||
An action to copy the contents of a panel to the clipboard.
|
||||
|
||||
Parameters:
|
||||
target_id (str): The ID of the target element containing the content to be copied
|
||||
"""
|
||||
template_name = 'ui/actions/copy_content.html'
|
||||
|
||||
def __init__(self, target_id, **kwargs):
|
||||
kwargs.setdefault('label', _('Copy'))
|
||||
kwargs.setdefault('button_icon', 'content-copy')
|
||||
super().__init__(**kwargs)
|
||||
self.target_id = target_id
|
||||
|
||||
def render(self, context):
|
||||
return render_to_string(self.template_name, {
|
||||
'target_id': self.target_id,
|
||||
'label': self.label,
|
||||
'button_class': self.button_class,
|
||||
'button_icon': self.button_icon,
|
||||
})
|
||||
344
netbox/netbox/ui/attrs.py
Normal file
344
netbox/netbox/ui/attrs.py
Normal file
@ -0,0 +1,344 @@
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.config import get_config
|
||||
from utilities.data import resolve_attr_path
|
||||
|
||||
__all__ = (
|
||||
'AddressAttr',
|
||||
'BooleanAttr',
|
||||
'ColorAttr',
|
||||
'ChoiceAttr',
|
||||
'GPSCoordinatesAttr',
|
||||
'ImageAttr',
|
||||
'NestedObjectAttr',
|
||||
'NumericAttr',
|
||||
'ObjectAttribute',
|
||||
'RelatedObjectAttr',
|
||||
'TemplatedAttr',
|
||||
'TextAttr',
|
||||
'TimezoneAttr',
|
||||
'UtilizationAttr',
|
||||
)
|
||||
|
||||
PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
|
||||
|
||||
|
||||
#
|
||||
# Attributes
|
||||
#
|
||||
|
||||
class ObjectAttribute:
|
||||
"""
|
||||
Base class for representing an attribute of an object.
|
||||
|
||||
Attributes:
|
||||
template_name (str): The name of the template to render
|
||||
placeholder (str): HTML to render for empty/null values
|
||||
|
||||
Parameters:
|
||||
accessor (str): The dotted path to the attribute being rendered (e.g. "site.region.name")
|
||||
label (str): Human-friendly label for the rendered attribute
|
||||
"""
|
||||
template_name = None
|
||||
label = None
|
||||
placeholder = mark_safe(PLACEHOLDER_HTML)
|
||||
|
||||
def __init__(self, accessor, label=None):
|
||||
self.accessor = accessor
|
||||
if label is not None:
|
||||
self.label = label
|
||||
|
||||
def get_value(self, obj):
|
||||
"""
|
||||
Return the value of the attribute.
|
||||
|
||||
Parameters:
|
||||
obj (object): The object for which the attribute is being rendered
|
||||
"""
|
||||
return resolve_attr_path(obj, self.accessor)
|
||||
|
||||
def get_context(self, obj, context):
|
||||
"""
|
||||
Return any additional template context used to render the attribute value.
|
||||
|
||||
Parameters:
|
||||
obj (object): The object for which the attribute is being rendered
|
||||
context (dict): The root template context
|
||||
"""
|
||||
return {}
|
||||
|
||||
def render(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
|
||||
# If the value is empty, render a placeholder
|
||||
if value in (None, ''):
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, context),
|
||||
'name': context['name'],
|
||||
'value': value,
|
||||
})
|
||||
|
||||
|
||||
class TextAttr(ObjectAttribute):
|
||||
"""
|
||||
A text attribute.
|
||||
|
||||
Parameters:
|
||||
style (str): CSS class to apply to the rendered attribute
|
||||
format_string (str): If specified, the value will be formatted using this string when rendering
|
||||
copy_button (bool): Set to True to include a copy-to-clipboard button
|
||||
"""
|
||||
template_name = 'ui/attrs/text.html'
|
||||
|
||||
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 get_value(self, obj):
|
||||
value = resolve_attr_path(obj, self.accessor)
|
||||
# Apply format string (if any)
|
||||
if value and self.format_string:
|
||||
return self.format_string.format(value)
|
||||
return value
|
||||
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
'style': self.style,
|
||||
'copy_button': self.copy_button,
|
||||
}
|
||||
|
||||
|
||||
class NumericAttr(ObjectAttribute):
|
||||
"""
|
||||
An integer or float attribute.
|
||||
|
||||
Parameters:
|
||||
unit_accessor (str): Accessor for the unit of measurement to display alongside the value (if any)
|
||||
copy_button (bool): Set to True to include a copy-to-clipboard button
|
||||
"""
|
||||
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 get_context(self, obj, context):
|
||||
unit = resolve_attr_path(obj, self.unit_accessor) if self.unit_accessor else None
|
||||
return {
|
||||
'unit': unit,
|
||||
'copy_button': self.copy_button,
|
||||
}
|
||||
|
||||
|
||||
class ChoiceAttr(ObjectAttribute):
|
||||
"""
|
||||
A selection from a set of choices.
|
||||
|
||||
The class calls get_FOO_display() on the object to retrieve the human-friendly choice label. If a get_FOO_color()
|
||||
method exists on the object, it will be used to render a background color for the attribute value.
|
||||
"""
|
||||
template_name = 'ui/attrs/choice.html'
|
||||
|
||||
def get_value(self, obj):
|
||||
try:
|
||||
return getattr(obj, f'get_{self.accessor}_display')()
|
||||
except AttributeError:
|
||||
return resolve_attr_path(obj, self.accessor)
|
||||
|
||||
def get_context(self, obj, context):
|
||||
try:
|
||||
bg_color = getattr(obj, f'get_{self.accessor}_color')()
|
||||
except AttributeError:
|
||||
bg_color = None
|
||||
return {
|
||||
'bg_color': bg_color,
|
||||
}
|
||||
|
||||
|
||||
class BooleanAttr(ObjectAttribute):
|
||||
"""
|
||||
A boolean attribute.
|
||||
|
||||
Parameters:
|
||||
display_false (bool): If False, a placeholder will be rendered instead of the "False" indication
|
||||
"""
|
||||
template_name = 'ui/attrs/boolean.html'
|
||||
|
||||
def __init__(self, *args, display_false=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.display_false = display_false
|
||||
|
||||
def get_value(self, obj):
|
||||
value = super().get_value(obj)
|
||||
if value is False and self.display_false is False:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
class ColorAttr(ObjectAttribute):
|
||||
"""
|
||||
An RGB color value.
|
||||
"""
|
||||
template_name = 'ui/attrs/color.html'
|
||||
label = _('Color')
|
||||
|
||||
|
||||
class ImageAttr(ObjectAttribute):
|
||||
"""
|
||||
An attribute representing an image field on the model. Displays the uploaded image.
|
||||
"""
|
||||
template_name = 'ui/attrs/image.html'
|
||||
|
||||
|
||||
class RelatedObjectAttr(ObjectAttribute):
|
||||
"""
|
||||
An attribute representing a related object.
|
||||
|
||||
Parameters:
|
||||
linkify (bool): If True, the rendered value will be hyperlinked to the related object's detail view
|
||||
grouped_by (str): A second-order object to annotate alongside the related object; for example, an attribute
|
||||
representing the dcim.Site model might specify grouped_by="region"
|
||||
"""
|
||||
template_name = 'ui/attrs/object.html'
|
||||
|
||||
def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.linkify = linkify
|
||||
self.grouped_by = grouped_by
|
||||
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
group = getattr(value, self.grouped_by, None) if self.grouped_by else None
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
'group': group,
|
||||
}
|
||||
|
||||
|
||||
class NestedObjectAttr(ObjectAttribute):
|
||||
"""
|
||||
An attribute representing a related nested object. Similar to `RelatedObjectAttr`, but includes the ancestors of the
|
||||
related object in the rendered output.
|
||||
|
||||
Parameters:
|
||||
linkify (bool): If True, the rendered value will be hyperlinked to the related object's detail view
|
||||
max_depth (int): Maximum number of ancestors to display (default: all)
|
||||
"""
|
||||
template_name = 'ui/attrs/nested_object.html'
|
||||
|
||||
def __init__(self, *args, linkify=None, max_depth=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.linkify = linkify
|
||||
self.max_depth = max_depth
|
||||
|
||||
def get_context(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
nodes = value.get_ancestors(include_self=True)
|
||||
if self.max_depth:
|
||||
nodes = list(nodes)[-self.max_depth:]
|
||||
return {
|
||||
'nodes': nodes,
|
||||
'linkify': self.linkify,
|
||||
}
|
||||
|
||||
|
||||
class AddressAttr(ObjectAttribute):
|
||||
"""
|
||||
A physical or mailing address.
|
||||
|
||||
Parameters:
|
||||
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
|
||||
"""
|
||||
template_name = 'ui/attrs/address.html'
|
||||
|
||||
def __init__(self, *args, map_url=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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 get_context(self, obj, context):
|
||||
return {
|
||||
'map_url': self.map_url,
|
||||
}
|
||||
|
||||
|
||||
class GPSCoordinatesAttr(ObjectAttribute):
|
||||
"""
|
||||
A GPS coordinates pair comprising latitude and longitude values.
|
||||
|
||||
Parameters:
|
||||
latitude_attr (float): The name of the field containing the latitude value
|
||||
longitude_attr (float): The name of the field containing the longitude value
|
||||
map_url (bool): If true, the address will render as a hyperlink using settings.MAPS_URL
|
||||
"""
|
||||
template_name = 'ui/attrs/gps_coordinates.html'
|
||||
label = _('GPS coordinates')
|
||||
|
||||
def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
|
||||
super().__init__(accessor=None, **kwargs)
|
||||
self.latitude_attr = latitude_attr
|
||||
self.longitude_attr = longitude_attr
|
||||
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, obj, context=None):
|
||||
context = context or {}
|
||||
latitude = resolve_attr_path(obj, self.latitude_attr)
|
||||
longitude = resolve_attr_path(obj, self.longitude_attr)
|
||||
if latitude is None or longitude is None:
|
||||
return self.placeholder
|
||||
return render_to_string(self.template_name, {
|
||||
**context,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'map_url': self.map_url,
|
||||
})
|
||||
|
||||
|
||||
class TimezoneAttr(ObjectAttribute):
|
||||
"""
|
||||
A timezone value. Includes the numeric offset from UTC.
|
||||
"""
|
||||
template_name = 'ui/attrs/timezone.html'
|
||||
|
||||
|
||||
class TemplatedAttr(ObjectAttribute):
|
||||
"""
|
||||
Renders an attribute using a custom template.
|
||||
|
||||
Parameters:
|
||||
template_name (str): The name of the template to render
|
||||
context (dict): Additional context to pass to the template when rendering
|
||||
"""
|
||||
def __init__(self, *args, template_name, context=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.template_name = template_name
|
||||
self.context = context or {}
|
||||
|
||||
def get_context(self, obj, context):
|
||||
return {
|
||||
**self.context,
|
||||
'object': obj,
|
||||
}
|
||||
|
||||
|
||||
class UtilizationAttr(ObjectAttribute):
|
||||
"""
|
||||
Renders the value of an attribute as a utilization graph.
|
||||
"""
|
||||
template_name = 'ui/attrs/utilization.html'
|
||||
94
netbox/netbox/ui/layout.py
Normal file
94
netbox/netbox/ui/layout.py
Normal file
@ -0,0 +1,94 @@
|
||||
from netbox.ui.panels import Panel, PluginContentPanel
|
||||
|
||||
__all__ = (
|
||||
'Column',
|
||||
'Layout',
|
||||
'Row',
|
||||
'SimpleLayout',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Base classes
|
||||
#
|
||||
|
||||
class Layout:
|
||||
"""
|
||||
A collection of rows and columns comprising the layout of content within the user interface.
|
||||
|
||||
Parameters:
|
||||
*rows: One or more Row instances
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Row:
|
||||
"""
|
||||
A collection of columns arranged horizontally.
|
||||
|
||||
Parameters:
|
||||
*columns: One or more Column instances
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Column:
|
||||
"""
|
||||
A collection of panels arranged vertically.
|
||||
|
||||
Parameters:
|
||||
*panels: One or more Panel instances
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
# Common layouts
|
||||
#
|
||||
|
||||
class SimpleLayout(Layout):
|
||||
"""
|
||||
A layout with one row of two columns and a second row with one column.
|
||||
|
||||
Plugin content registered for `left_page`, `right_page`, or `full_width_path` is included automatically. Most object
|
||||
views in NetBox utilize this layout.
|
||||
|
||||
```
|
||||
+-------+-------+
|
||||
| Col 1 | Col 2 |
|
||||
+-------+-------+
|
||||
| Col 3 |
|
||||
+---------------+
|
||||
```
|
||||
|
||||
Parameters:
|
||||
left_panels: Panel instances to be rendered in the top lefthand column
|
||||
right_panels: Panel instances to be rendered in the top righthand column
|
||||
bottom_panels: Panel instances to be rendered in the bottom row
|
||||
"""
|
||||
def __init__(self, left_panels=None, right_panels=None, bottom_panels=None):
|
||||
left_panels = left_panels or []
|
||||
right_panels = right_panels or []
|
||||
bottom_panels = bottom_panels or []
|
||||
rows = [
|
||||
Row(
|
||||
Column(*left_panels, PluginContentPanel('left_page')),
|
||||
Column(*right_panels, PluginContentPanel('right_page')),
|
||||
),
|
||||
Row(
|
||||
Column(*bottom_panels, PluginContentPanel('full_width_page'))
|
||||
)
|
||||
]
|
||||
super().__init__(*rows)
|
||||
341
netbox/netbox/ui/panels.py
Normal file
341
netbox/netbox/ui/panels.py
Normal file
@ -0,0 +1,341 @@
|
||||
from django.apps import apps
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs
|
||||
from netbox.ui.actions import CopyContent
|
||||
from utilities.data import resolve_attr_path
|
||||
from utilities.querydict import dict_to_querydict
|
||||
from utilities.string import title
|
||||
from utilities.templatetags.plugins import _get_registered_content
|
||||
from utilities.views import get_viewname
|
||||
|
||||
__all__ = (
|
||||
'CommentsPanel',
|
||||
'JSONPanel',
|
||||
'NestedGroupObjectPanel',
|
||||
'ObjectAttributesPanel',
|
||||
'ObjectPanel',
|
||||
'ObjectsTablePanel',
|
||||
'OrganizationalObjectPanel',
|
||||
'Panel',
|
||||
'PluginContentPanel',
|
||||
'RelatedObjectsPanel',
|
||||
'TemplatePanel',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Base classes
|
||||
#
|
||||
|
||||
class Panel:
|
||||
"""
|
||||
A block of content rendered within an HTML template.
|
||||
|
||||
Panels are arranged within rows and columns, (generally) render as discrete "cards" within the user interface. Each
|
||||
panel has a title and may have one or more actions associated with it, which will be rendered as hyperlinks in the
|
||||
top right corner of the card.
|
||||
|
||||
Attributes:
|
||||
template_name (str): The name of the template used to render the panel
|
||||
|
||||
Parameters:
|
||||
title (str): The human-friendly title of the panel
|
||||
actions (list): An iterable of PanelActions to include in the panel header
|
||||
"""
|
||||
template_name = None
|
||||
title = None
|
||||
actions = None
|
||||
|
||||
def __init__(self, title=None, actions=None):
|
||||
if title is not None:
|
||||
self.title = title
|
||||
self.actions = actions or self.actions or []
|
||||
|
||||
def get_context(self, context):
|
||||
"""
|
||||
Return the context data to be used when rendering the panel.
|
||||
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
return {
|
||||
'request': context.get('request'),
|
||||
'object': context.get('object'),
|
||||
'title': self.title,
|
||||
'actions': self.actions,
|
||||
'panel_class': self.__class__.__name__,
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
"""
|
||||
Render the panel as HTML.
|
||||
|
||||
Parameters:
|
||||
context (dict): The template context
|
||||
"""
|
||||
return render_to_string(self.template_name, self.get_context(context))
|
||||
|
||||
|
||||
#
|
||||
# Object-specific panels
|
||||
#
|
||||
|
||||
class ObjectPanel(Panel):
|
||||
"""
|
||||
Base class for object-specific panels.
|
||||
|
||||
Parameters:
|
||||
accessor (str): The dotted path in context data to the object being rendered (default: "object")
|
||||
"""
|
||||
accessor = 'object'
|
||||
|
||||
def __init__(self, accessor=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if accessor is not None:
|
||||
self.accessor = accessor
|
||||
|
||||
def get_context(self, context):
|
||||
obj = resolve_attr_path(context, self.accessor)
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'title': self.title or title(obj._meta.verbose_name),
|
||||
'object': obj,
|
||||
}
|
||||
|
||||
|
||||
class ObjectAttributesPanelMeta(type):
|
||||
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
declared = {}
|
||||
|
||||
# Walk MRO parents (excluding `object`) for declared attributes
|
||||
for base in reversed([b for b in bases if hasattr(b, "_attrs")]):
|
||||
for key, attr in getattr(base, '_attrs', {}).items():
|
||||
if key not in declared:
|
||||
declared[key] = attr
|
||||
|
||||
# Add local declarations in the order they appear in the class body
|
||||
for key, attr in namespace.items():
|
||||
if isinstance(attr, attrs.ObjectAttribute):
|
||||
declared[key] = attr
|
||||
|
||||
namespace['_attrs'] = declared
|
||||
|
||||
# Remove Attrs from the class namespace to keep things tidy
|
||||
local_items = [key for key, attr in namespace.items() if isinstance(attr, attrs.ObjectAttribute)]
|
||||
for key in local_items:
|
||||
namespace.pop(key)
|
||||
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
return cls
|
||||
|
||||
|
||||
class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
||||
"""
|
||||
A panel which displays selected attributes of an object.
|
||||
|
||||
Attributes are added to the panel by declaring ObjectAttribute instances in the class body (similar to fields on
|
||||
a Django form). Attributes are displayed in the order they are declared.
|
||||
|
||||
Note that the `only` and `exclude` parameters are mutually exclusive.
|
||||
|
||||
Parameters:
|
||||
only (list): If specified, only attributes in this list will be displayed
|
||||
exclude (list): If specified, attributes in this list will be excluded from display
|
||||
"""
|
||||
template_name = 'ui/panels/object_attributes.html'
|
||||
|
||||
def __init__(self, only=None, exclude=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Set included/excluded attributes
|
||||
if only is not None and exclude is not None:
|
||||
raise ValueError("only and exclude cannot both be specified.")
|
||||
self.only = only or []
|
||||
self.exclude = exclude or []
|
||||
|
||||
@staticmethod
|
||||
def _name_to_label(name):
|
||||
"""
|
||||
Format an attribute's name to be presented as a human-friendly label.
|
||||
"""
|
||||
label = name[:1].upper() + name[1:]
|
||||
label = label.replace('_', ' ')
|
||||
return label
|
||||
|
||||
def get_context(self, context):
|
||||
# Determine which attributes to display in the panel based on only/exclude args
|
||||
attr_names = set(self._attrs.keys())
|
||||
if self.only:
|
||||
attr_names &= set(self.only)
|
||||
elif self.exclude:
|
||||
attr_names -= set(self.exclude)
|
||||
|
||||
ctx = super().get_context(context)
|
||||
|
||||
return {
|
||||
**ctx,
|
||||
'attrs': [
|
||||
{
|
||||
'label': attr.label or self._name_to_label(name),
|
||||
'value': attr.render(ctx['object'], {'name': name}),
|
||||
} for name, attr in self._attrs.items() if name in attr_names
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class OrganizationalObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
|
||||
"""
|
||||
An ObjectPanel with attributes common to OrganizationalModels. Includes `name` and `description` attributes.
|
||||
"""
|
||||
name = attrs.TextAttr('name', label=_('Name'))
|
||||
description = attrs.TextAttr('description', label=_('Description'))
|
||||
|
||||
|
||||
class NestedGroupObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
|
||||
"""
|
||||
An ObjectPanel with attributes common to NestedGroupObjects. Includes the `parent` attribute.
|
||||
"""
|
||||
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
|
||||
name = attrs.TextAttr('name', label=_('Name'))
|
||||
description = attrs.TextAttr('description', label=_('Description'))
|
||||
|
||||
|
||||
class CommentsPanel(ObjectPanel):
|
||||
"""
|
||||
A panel which displays comments associated with an object.
|
||||
|
||||
Parameters:
|
||||
field_name (str): The name of the comment field on the object (default: "comments")
|
||||
"""
|
||||
template_name = 'ui/panels/comments.html'
|
||||
title = _('Comments')
|
||||
|
||||
def __init__(self, field_name='comments', **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.field_name = field_name
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'comments': getattr(context['object'], self.field_name),
|
||||
}
|
||||
|
||||
|
||||
class JSONPanel(ObjectPanel):
|
||||
"""
|
||||
A panel which renders formatted JSON data from an object's JSONField.
|
||||
|
||||
Parameters:
|
||||
field_name (str): The name of the JSON field on the object
|
||||
copy_button (bool): Set to True (default) to include a copy-to-clipboard button
|
||||
"""
|
||||
template_name = 'ui/panels/json.html'
|
||||
|
||||
def __init__(self, field_name, copy_button=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.field_name = field_name
|
||||
|
||||
if copy_button:
|
||||
self.actions.append(CopyContent(f'panel_{field_name}'))
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'data': getattr(context['object'], self.field_name),
|
||||
'field_name': self.field_name,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous panels
|
||||
#
|
||||
|
||||
class RelatedObjectsPanel(Panel):
|
||||
"""
|
||||
A panel which displays the types and counts of related objects.
|
||||
"""
|
||||
template_name = 'ui/panels/related_objects.html'
|
||||
title = _('Related Objects')
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'related_models': context.get('related_models'),
|
||||
}
|
||||
|
||||
|
||||
class ObjectsTablePanel(Panel):
|
||||
"""
|
||||
A panel which displays a table of objects (rendered via HTMX).
|
||||
|
||||
Parameters:
|
||||
model (str): The dotted label of the model to be added (e.g. "dcim.site")
|
||||
filters (dict): A dictionary of arbitrary URL parameters to append to the table's URL. If the value of a key is
|
||||
a callable, it will be passed the current template context.
|
||||
"""
|
||||
template_name = 'ui/panels/objects_table.html'
|
||||
title = None
|
||||
|
||||
def __init__(self, model, filters=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Resolve the model class from its app.name label
|
||||
try:
|
||||
app_label, model_name = model.split('.')
|
||||
self.model = apps.get_model(app_label, model_name)
|
||||
except (ValueError, LookupError):
|
||||
raise ValueError(f"Invalid model label: {model}")
|
||||
|
||||
self.filters = filters or {}
|
||||
|
||||
# If no title is specified, derive one from the model name
|
||||
if self.title is None:
|
||||
self.title = title(self.model._meta.verbose_name_plural)
|
||||
|
||||
def get_context(self, context):
|
||||
url_params = {
|
||||
k: v(context) if callable(v) else v for k, v in self.filters.items()
|
||||
}
|
||||
if 'return_url' not in url_params and 'object' in context:
|
||||
url_params['return_url'] = context['object'].get_absolute_url()
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'viewname': get_viewname(self.model, 'list'),
|
||||
'url_params': dict_to_querydict(url_params),
|
||||
}
|
||||
|
||||
|
||||
class TemplatePanel(Panel):
|
||||
"""
|
||||
A panel which renders custom content using an HTML template.
|
||||
|
||||
Parameters:
|
||||
template_name (str): The name of the template to render
|
||||
"""
|
||||
def __init__(self, template_name, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.template_name = template_name
|
||||
|
||||
def render(self, context):
|
||||
# Pass the entire context to the template
|
||||
return render_to_string(self.template_name, context.flatten())
|
||||
|
||||
|
||||
class PluginContentPanel(Panel):
|
||||
"""
|
||||
A panel which displays embedded plugin content.
|
||||
|
||||
Parameters:
|
||||
method (str): The name of the plugin method to render (e.g. "left_page")
|
||||
"""
|
||||
def __init__(self, method, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.method = method
|
||||
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
return _get_registered_content(obj, self.method, context)
|
||||
@ -44,9 +44,11 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
||||
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
|
||||
|
||||
Attributes:
|
||||
layout: An instance of `netbox.ui.layout.Layout` which defines the page layout (overrides HTML template)
|
||||
tab: A ViewTab instance for the view
|
||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||
"""
|
||||
layout = None
|
||||
tab = None
|
||||
actions = (CloneObject, EditObject, DeleteObject)
|
||||
|
||||
@ -81,6 +83,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
||||
'object': instance,
|
||||
'actions': actions,
|
||||
'tab': self.tab,
|
||||
'layout': self.layout,
|
||||
**self.get_extra_context(request, instance),
|
||||
})
|
||||
|
||||
|
||||
@ -1,375 +1 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Device" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>{% nested_tree object.site.region %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
{% if object.virtual_chassis %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Chassis" %}</th>
|
||||
<td>{{ object.virtual_chassis|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td class="d-flex justify-content-between align-items-start">
|
||||
{% if object.rack %}
|
||||
{{ object.rack|linkify }}
|
||||
<a href="{{ object.rack.get_absolute_url }}?device={% firstof object.parent_bay.device.pk object.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>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Position" %}</th>
|
||||
<td>
|
||||
{% if object.parent_bay %}
|
||||
{% with object.parent_bay.device as parent %}
|
||||
{{ parent|linkify }} / {{ object.parent_bay }}
|
||||
{% if parent.position %}
|
||||
(U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% elif object.rack and object.position %}
|
||||
<span>U{{ object.position|floatformat }} / {{ object.get_face_display }}</span>
|
||||
{% elif object.rack and object.device_type.u_height %}
|
||||
<span class="badge text-bg-warning">{% trans "Not racked" %}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "GPS Coordinates" %}</th>
|
||||
<td class="position-relative">
|
||||
{% if object.latitude and object.longitude %}
|
||||
{% if config.MAPS_URL %}
|
||||
<div class="position-absolute top-50 end-0 me-2 translate-middle-y d-print-none">
|
||||
<a href="{{ config.MAPS_URL }}{{ object.latitude|unlocalize }},{{ object.longitude|unlocalize }}" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-map-marker"></i> {% trans "Map" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device Type" %}</th>
|
||||
<td>
|
||||
{{ object.device_type|linkify:"full_name" }} ({{ object.device_type.u_height|floatformat }}U)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>
|
||||
{{ object.get_airflow_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% if vc_members %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual Chassis" %}
|
||||
<div class="card-actions">
|
||||
<a href="{{ object.virtual_chassis.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-arrow-right-bold" aria-hidden="true"></span> {% trans "View Virtual Chassis" %}
|
||||
</a>
|
||||
</div>
|
||||
</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<thead>
|
||||
<tr class="border-bottom">
|
||||
<th>{% trans "Device" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Master" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vc_member in vc_members %}
|
||||
<tr{% if vc_member == object %} class="table-primary"{% endif %}>
|
||||
<td>{{ vc_member|linkify }}</td>
|
||||
<td>{% badge vc_member.vc_position show_empty=True %}</td>
|
||||
<td>
|
||||
{% if object.virtual_chassis.master == vc_member %}
|
||||
{% checkmark True %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vc_member.vc_priority|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual Device Contexts" %}
|
||||
{% if perms.dcim.add_virtualdevicecontext %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:virtualdevicecontext_add' %}?device={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Create VDC" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:virtualdevicecontext_list' device_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Management" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Platform" %}</th>
|
||||
<td>{{ object.platform|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary IPv4" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% if object.primary_ip4.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip4.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% copy_content "primary_ip4" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary IPv6" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% if object.primary_ip6.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip6.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% copy_content "primary_ip6" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Out-of-band IP</th>
|
||||
<td>
|
||||
{% if object.oob_ip %}
|
||||
<a href="{{ object.oob_ip.get_absolute_url }}" id="oob_ip">{{ object.oob_ip.address.ip }}</a>
|
||||
{% if object.oob_ip.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ object.oob_ip.nat_inside.get_absolute_url }}">{{ object.oob_ip.nat_inside.address.ip }}</a>)
|
||||
{% elif object.oob_ip.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in object.oob_ip.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% copy_content "oob_ip" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if object.cluster %}
|
||||
<tr>
|
||||
<th>{% trans "Cluster" %}</th>
|
||||
<td>
|
||||
{% if object.cluster.group %}
|
||||
{{ object.cluster.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.cluster|linkify }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% if object.powerports.exists and object.poweroutlets.exists %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Power Utilization" %}</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Input" %}</th>
|
||||
<th>{% trans "Outlets" %}</th>
|
||||
<th>{% trans "Allocated" %}</th>
|
||||
<th>{% trans "Available" %}</th>
|
||||
<th>{% trans "Utilization" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for powerport in object.powerports.all %}
|
||||
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %}
|
||||
<tr>
|
||||
<td>{{ powerport }}</td>
|
||||
<td>{{ utilization.outlet_count }}</td>
|
||||
<td>{{ utilization.allocated }}{% trans "VA" %}</td>
|
||||
{% if powerfeed.available_power %}
|
||||
<td>{{ powerfeed.available_power }}{% trans "VA" %}</td>
|
||||
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
|
||||
{% else %}
|
||||
<td class="text-muted">—</td>
|
||||
<td class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for leg in utilization.legs %}
|
||||
<tr>
|
||||
<td style="padding-left: 20px">
|
||||
{% trans "Leg" context "Leg of a power feed" %} {{ leg.name }}
|
||||
</td>
|
||||
<td>{{ leg.outlet_count }}</td>
|
||||
<td>{{ leg.allocated }}</td>
|
||||
{% if powerfeed.available_power %}
|
||||
{% with phase_available=powerfeed.available_power|divide:3 %}
|
||||
<td>{{ phase_available }}{% trans "VA" %}</td>
|
||||
<td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td class="text-muted">—</td>
|
||||
<td class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Application Services" %}
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:service_list' device_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Dimensions" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Height" %}</th>
|
||||
<td>
|
||||
{{ object.device_type.u_height }}U
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>
|
||||
{% if object.total_weight %}
|
||||
{{ object.total_weight|floatformat }} {% trans "Kilograms" %}
|
||||
({{ object.total_weight|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% if object.rack and object.position %}
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="text-center">
|
||||
<strong><a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack.name }}</a></strong>
|
||||
{% if object.rack.role %}
|
||||
<br /><span class="badge my-3" style="color: {{ object.rack.role.color|fgcolor }}; background-color: #{{ object.rack.role.color }}">{{ object.rack.role }}</span>
|
||||
{% endif %}
|
||||
{% if object.rack.facility_id %}
|
||||
<br /><small class="text-muted">{{ object.rack.facility_id }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
10
netbox/templates/dcim/device/attrs/ipaddress.html
Normal file
10
netbox/templates/dcim/device/attrs/ipaddress.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
|
||||
{% if value.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
|
||||
{% elif value.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
10
netbox/templates/dcim/device/attrs/parent_device.html
Normal file
10
netbox/templates/dcim/device/attrs/parent_device.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
<ol class="breadcrumb" aria-label="breadcrumbs">
|
||||
<li class="breadcrumb-item">{{ value.device|linkify }}</li>
|
||||
<li class="breadcrumb-item">{{ value }}</li>
|
||||
</ol>
|
||||
{% if value.device.position %}
|
||||
<a href="{{ value.device.rack.get_absolute_url }}?device={{ value.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 %}
|
||||
14
netbox/templates/dcim/device/attrs/rack.html
Normal file
14
netbox/templates/dcim/device/attrs/rack.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% load i18n %}
|
||||
<span>
|
||||
{{ value|linkify }}
|
||||
{% if value and object.position %}
|
||||
(U{{ object.position|floatformat }} / {{ object.get_face_display }})
|
||||
{% elif value and object.device_type.u_height %}
|
||||
<span class="badge text-bg-warning">{% trans "Not racked" %}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if object.position %}
|
||||
<a href="{{ value.get_absolute_url }}?device={{ object.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 %}
|
||||
3
netbox/templates/dcim/device/attrs/total_weight.html
Normal file
3
netbox/templates/dcim/device/attrs/total_weight.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% load helpers i18n %}
|
||||
{{ value|floatformat }} {% trans "Kilograms" %}
|
||||
({{ value|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
@ -1,112 +1 @@
|
||||
{% extends 'dcim/devicetype/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Chassis" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Model Name" %}</th>
|
||||
<td>
|
||||
{{ object.model }}<br/>
|
||||
<small class="text-muted">{{ object.slug }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Part Number" %}</th>
|
||||
<td>{{ object.part_number|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Default Platform" %}</th>
|
||||
<td>{{ object.default_platform|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Height (U)" %}</th>
|
||||
<td>{{ object.u_height|floatformat }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Exclude From Utilization" %}</th>
|
||||
<td>{% checkmark object.exclude_from_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Full Depth" %}</th>
|
||||
<td>{% checkmark object.is_full_depth %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent/Child" %}</th>
|
||||
<td>
|
||||
{{ object.get_subdevice_role_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>
|
||||
{{ object.get_airflow_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Front Image" %}</th>
|
||||
<td>
|
||||
{% if object.front_image %}
|
||||
<a href="{{ object.front_image.url }}">
|
||||
<img src="{{ object.front_image.url }}" alt="{{ object.front_image.name }}" class="img-fluid" />
|
||||
</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rear Image" %}</th>
|
||||
<td>
|
||||
{% if object.rear_image %}
|
||||
<a href="{{ object.rear_image.url }}">
|
||||
<img src="{{ object.rear_image.url }}" alt="{{ object.rear_image.name }}" class="img-fluid" />
|
||||
</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -10,96 +6,3 @@
|
||||
<li class="breadcrumb-item">{{ location|linkify }}</li>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_location %}
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Child Location" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Location" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Facility" %}</th>
|
||||
<td>{{ object.facility|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Locations" %}
|
||||
{% if perms.dcim.add_location %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Location" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:location_list' parent_id=object.pk %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Non-Racked Devices" %}
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:device_list' location_id=object.pk rack_id='null' parent_bay_id='null' %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -25,35 +22,3 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Manufacturer" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,59 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ object.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Module Type Profile" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header d-flex justify-content-between">
|
||||
{% trans "Schema" %}
|
||||
{% copy_content 'profile_schema' %}
|
||||
</h2>
|
||||
<pre id="profile_schema">{{ object.schema|json }}</pre>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Module Types" %}
|
||||
{% if perms.dcim.add_moduletype %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:moduletype_add' %}?profile={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Module Type" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:moduletype_list' profile_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
26
netbox/templates/dcim/panels/device_rack_elevations.html
Normal file
26
netbox/templates/dcim/panels/device_rack_elevations.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% load i18n %}
|
||||
{% if object.rack and object.position %}
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="text-center">
|
||||
<strong><a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack.name }}</a></strong>
|
||||
{% if object.rack.role %}
|
||||
<br /><span class="badge my-3" style="color: {{ object.rack.role.color|fgcolor }}; background-color: #{{ object.rack.role.color }}">{{ object.rack.role }}</span>
|
||||
{% endif %}
|
||||
{% if object.rack.facility_id %}
|
||||
<br /><small class="text-muted">{{ object.rack.facility_id }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
50
netbox/templates/dcim/panels/power_utilization.html
Normal file
50
netbox/templates/dcim/panels/power_utilization.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Input" %}</th>
|
||||
<th>{% trans "Outlets" %}</th>
|
||||
<th>{% trans "Allocated" %}</th>
|
||||
<th>{% trans "Available" %}</th>
|
||||
<th>{% trans "Utilization" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for powerport in object.powerports.all %}
|
||||
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %}
|
||||
<tr>
|
||||
<td>{{ powerport }}</td>
|
||||
<td>{{ utilization.outlet_count }}</td>
|
||||
<td>{{ utilization.allocated }}{% trans "VA" %}</td>
|
||||
{% if powerfeed.available_power %}
|
||||
<td>{{ powerfeed.available_power }}{% trans "VA" %}</td>
|
||||
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
|
||||
{% else %}
|
||||
<td class="text-muted">—</td>
|
||||
<td class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for leg in utilization.legs %}
|
||||
<tr>
|
||||
<td style="padding-left: 20px">
|
||||
{% trans "Leg" context "Leg of a power feed" %} {{ leg.name }}
|
||||
</td>
|
||||
<td>{{ leg.outlet_count }}</td>
|
||||
<td>{{ leg.allocated }}</td>
|
||||
{% if powerfeed.available_power %}
|
||||
{% with phase_available=powerfeed.available_power|divide:3 %}
|
||||
<td>{{ phase_available }}{% trans "VA" %}</td>
|
||||
<td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td class="text-muted">—</td>
|
||||
<td class="text-muted">—</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
22
netbox/templates/dcim/panels/rack_elevations.html
Normal file
22
netbox/templates/dcim/panels/rack_elevations.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
<div class="text-end mb-4">
|
||||
<select class="btn btn-outline-secondary no-ts rack-view">
|
||||
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
||||
<option value="images-only">{% trans "Images only" %}</option>
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,15 @@
|
||||
{% load i18n %}
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
31
netbox/templates/dcim/panels/virtual_chassis_members.html
Normal file
31
netbox/templates/dcim/panels/virtual_chassis_members.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
<thead>
|
||||
<tr class="border-bottom">
|
||||
<th>{% trans "Device" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Master" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for vc_member in vc_members %}
|
||||
<tr{% if vc_member == object %} class="table-primary"{% endif %}>
|
||||
<td>{{ vc_member|linkify }}</td>
|
||||
<td>{% badge vc_member.vc_position show_empty=True %}</td>
|
||||
<td>
|
||||
{% if object.virtual_chassis.master == vc_member %}
|
||||
{% checkmark True %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vc_member.vc_priority|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
@ -1,153 +1 @@
|
||||
{% extends 'dcim/rack/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-xl-5">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>{% nested_tree object.site.region %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Facility ID" %}</th>
|
||||
<td>{{ object.facility_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Type" %}</th>
|
||||
<td>{{ object.rack_type|linkify:"full_name"|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Space Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Power Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_power_utilization %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||
{% include 'dcim/inc/panels/racktype_numbering.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Weight" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maximum Weight" %}</th>
|
||||
<td>
|
||||
{% if object.max_weight %}
|
||||
{{ object.max_weight }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Total Weight" %}</th>
|
||||
<td>
|
||||
{% if object.total_weight %}
|
||||
{{ object.total_weight|floatformat }} {% trans "Kilograms" %}
|
||||
({{ object.total_weight|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<div class="text-end mb-4">
|
||||
<select class="btn btn-outline-secondary no-ts rack-view">
|
||||
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
||||
<option value="images-only">{% trans "Images only" %}</option>
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
3
netbox/templates/dcim/rack/attrs/total_weight.html
Normal file
3
netbox/templates/dcim/rack/attrs/total_weight.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% load helpers i18n %}
|
||||
{{ value|floatformat }} {% trans "Kilograms" %}
|
||||
({{ value|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
@ -1,99 +1,8 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:rackreservation_list' %}?rack_id={{ object.rack.pk }}">{{ object.rack }}</a></li>
|
||||
<li class="breadcrumb-item">{% trans "Units" %} {{ object.unit_list }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-xl-5">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.rack.site.region %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.rack.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{{ object.rack.location|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td>{{ object.rack|linkify }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Reservation Details" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Units" %}</th>
|
||||
<td>{{ object.unit_list }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "User" %}</th>
|
||||
<td>{{ object.user }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: -30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -11,41 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack Role" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Color" %}</th>
|
||||
<td>
|
||||
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,75 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack Type" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Model" %}</th>
|
||||
<td>{{ object.model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'dcim/inc/panels/racktype_numbering.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Weight" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maximum Weight" %}</th>
|
||||
<td>
|
||||
{% if object.max_weight %}
|
||||
{{ object.max_weight }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@ -18,53 +15,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Region" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Regions" %}
|
||||
{% if perms.dcim.add_region %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:region_add' %}?parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Region" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:region_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load tz %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -20,135 +14,3 @@
|
||||
<li class="breadcrumb-item"><a href="{% url 'dcim:site_list' %}?group_id={{ object.group.pk }}">{{ object.group }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Site" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.region %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.group %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Facility" %}</th>
|
||||
<td>{{ object.facility|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Time Zone" %}</th>
|
||||
<td>
|
||||
{% if object.time_zone %}
|
||||
{{ object.time_zone }} ({% trans "UTC" %} {{ object.time_zone|tzoffset }})<br />
|
||||
<small class="text-muted">{% trans "Site time" %}: {% timezone object.time_zone %}{% now 'Y-m-d H:i' %}{% endtimezone %}</small>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Physical Address" %}</th>
|
||||
<td class="d-flex justify-content-between align-items-start">
|
||||
{% if object.physical_address %}
|
||||
<span>{{ object.physical_address|linebreaksbr }}</span>
|
||||
{% if config.MAPS_URL %}
|
||||
<a href="{{ config.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-sm d-print-none">
|
||||
<i class="mdi mdi-map-marker"></i> {% trans "Map" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Shipping Address" %}</th>
|
||||
<td>{{ object.shipping_address|linebreaksbr|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "GPS Coordinates" %}</th>
|
||||
<td class="position-relative">
|
||||
{% if object.latitude and object.longitude %}
|
||||
{% if config.MAPS_URL %}
|
||||
<div class="position-absolute top-50 end-0 me-2 translate-middle-y d-print-none">
|
||||
<a href="{{ config.MAPS_URL }}{{ object.latitude|unlocalize }},{{ object.longitude|unlocalize }}" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-map-marker"></i> {% trans "Map" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span>{{ object.latitude }}, {{ object.longitude }}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Locations" %}
|
||||
{% if perms.dcim.add_location %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:location_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Location" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:location_list' site_id=object.pk %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Non-Racked Devices" %}
|
||||
{% if perms.dcim.add_device %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:device_list' site_id=object.pk rack_id='null' parent_bay_id='null' %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@ -18,53 +15,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Site Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Groups" %}
|
||||
{% if perms.dcim.add_sitegroup %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:sitegroup_add' %}?parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Site Group" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:sitegroup_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
31
netbox/templates/extras/panels/custom_fields.html
Normal file
31
netbox/templates/extras/panels/custom_fields.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for group_name, fields in custom_fields.items %}
|
||||
{% if group_name %}
|
||||
<tr>
|
||||
<th scope="row" colspan="2" class="fw-bold">{{ group_name }}</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for field, value in fields.items %}
|
||||
<tr>
|
||||
<th scope="row"{% if group_name %} class="ps-3"{% endif %}>{{ field }}
|
||||
{% if field.description %}
|
||||
<i
|
||||
class="mdi mdi-information text-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
title="{{ field.description|escape }}"
|
||||
></i>
|
||||
{% endif %}
|
||||
</th>
|
||||
<td>
|
||||
{% customfield_value field value %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
15
netbox/templates/extras/panels/tags.html
Normal file
15
netbox/templates/extras/panels/tags.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<div class="card-body">
|
||||
{% with url=object|validated_viewname:"list" %}
|
||||
{% for tag in object.tags.all %}
|
||||
{% tag tag url %}
|
||||
{% empty %}
|
||||
<span class="text-muted">{% trans "No tags assigned" %}</span>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endblock panel_content %}
|
||||
@ -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 %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{% include 'inc/htmx_modal.html' %}
|
||||
|
||||
7
netbox/templates/ui/actions/copy_content.html
Normal file
7
netbox/templates/ui/actions/copy_content.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
<a class="btn btn-ghost-{{ button_class }} btn-sm copy-content" data-clipboard-target="#{{ target_id }}" title="{% trans "Copy to clipboard" %}">
|
||||
{% if button_icon %}
|
||||
<i class="mdi mdi-{{ button_icon }}" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
{{ label }}
|
||||
</a>
|
||||
6
netbox/templates/ui/actions/link.html
Normal file
6
netbox/templates/ui/actions/link.html
Normal file
@ -0,0 +1,6 @@
|
||||
<a {% if url %}href="{{ url }}" {% endif %}class="btn btn-ghost-{{ button_class }} btn-sm">
|
||||
{% if button_icon %}
|
||||
<i class="mdi mdi-{{ button_icon }}" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
{{ label }}
|
||||
</a>
|
||||
8
netbox/templates/ui/attrs/address.html
Normal file
8
netbox/templates/ui/attrs/address.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
<span>{{ value|linebreaksbr }}</span>
|
||||
{% if map_url %}
|
||||
<a href="{{ map_url }}{{ value }}" target="_blank" class="btn btn-primary btn-sm print-none">
|
||||
<i class="mdi mdi-map-marker"></i> {% trans "Map" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
1
netbox/templates/ui/attrs/boolean.html
Normal file
1
netbox/templates/ui/attrs/boolean.html
Normal file
@ -0,0 +1 @@
|
||||
{% checkmark value %}
|
||||
5
netbox/templates/ui/attrs/choice.html
Normal file
5
netbox/templates/ui/attrs/choice.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% if bg_color %}
|
||||
{% badge value bg_color=bg_color %}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
1
netbox/templates/ui/attrs/color.html
Normal file
1
netbox/templates/ui/attrs/color.html
Normal file
@ -0,0 +1 @@
|
||||
<span class="badge color-label" style="background-color: #{{ value }}"> </span>
|
||||
8
netbox/templates/ui/attrs/gps_coordinates.html
Normal file
8
netbox/templates/ui/attrs/gps_coordinates.html
Normal file
@ -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 %}
|
||||
3
netbox/templates/ui/attrs/image.html
Normal file
3
netbox/templates/ui/attrs/image.html
Normal file
@ -0,0 +1,3 @@
|
||||
<a href="{{ value.url }}">
|
||||
<img src="{{ value.url }}" alt="{{ value.name }}" class="img-fluid" />
|
||||
</a>
|
||||
11
netbox/templates/ui/attrs/nested_object.html
Normal file
11
netbox/templates/ui/attrs/nested_object.html
Normal file
@ -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>
|
||||
12
netbox/templates/ui/attrs/numeric.html
Normal file
12
netbox/templates/ui/attrs/numeric.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% load i18n %}
|
||||
<span{% if style %} class="{{ style }}"{% endif %}>
|
||||
<span{% if name %} id="attr_{{ name }}"{% endif %}>{{ value }}</span>
|
||||
{% if unit %}
|
||||
{{ unit|lower }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if copy_button %}
|
||||
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
14
netbox/templates/ui/attrs/object.html
Normal file
14
netbox/templates/ui/attrs/object.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% if group %}
|
||||
{# Display an object with its parent group #}
|
||||
<ol class="breadcrumb" aria-label="breadcrumbs">
|
||||
<li class="breadcrumb-item">
|
||||
{% if linkify %}{{ group|linkify }}{% else %}{{ group }}{% endif %}
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
{% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
{% else %}
|
||||
{# Display only the object #}
|
||||
{% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}
|
||||
{% endif %}
|
||||
7
netbox/templates/ui/attrs/text.html
Normal file
7
netbox/templates/ui/attrs/text.html
Normal file
@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
<span{% if name %} id="attr_{{ name }}"{% endif %}{% if style %} class="{{ style }}"{% endif %}>{{ value }}</span>
|
||||
{% if copy_button %}
|
||||
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
6
netbox/templates/ui/attrs/timezone.html
Normal file
6
netbox/templates/ui/attrs/timezone.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load tz %}
|
||||
<div>
|
||||
{{ value }} ({% trans "UTC" %} {{ value|tzoffset }})<br />
|
||||
<small class="text-muted">{% trans "Local time" %}: {% timezone value %}{% now 'Y-m-d H:i' %}{% endtimezone %}</small>
|
||||
</div>
|
||||
2
netbox/templates/ui/attrs/utilization.html
Normal file
2
netbox/templates/ui/attrs/utilization.html
Normal file
@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
15
netbox/templates/ui/panels/_base.html
Normal file
15
netbox/templates/ui/panels/_base.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!-- begin {{ panel_class|default:"panel" }} -->
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{{ title }}
|
||||
{% if actions %}
|
||||
<div class="card-actions">
|
||||
{% for action in actions %}
|
||||
{% render action %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% block panel_content %}{% endblock %}
|
||||
</div>
|
||||
<!-- end {{ panel_class|default:"panel" }} -->
|
||||
12
netbox/templates/ui/panels/comments.html
Normal file
12
netbox/templates/ui/panels/comments.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<div class="card-body">
|
||||
{% if comments %}
|
||||
{{ comments|markdown }}
|
||||
{% else %}
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock panel_content %}
|
||||
5
netbox/templates/ui/panels/embedded_table.html
Normal file
5
netbox/templates/ui/panels/embedded_table.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include 'builtins/htmx_table.html' %}
|
||||
{% endblock panel_content %}
|
||||
5
netbox/templates/ui/panels/json.html
Normal file
5
netbox/templates/ui/panels/json.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
|
||||
{% block panel_content %}
|
||||
<pre id="panel_{{ field_name }}">{{ data|json }}</pre>
|
||||
{% endblock panel_content %}
|
||||
14
netbox/templates/ui/panels/object_attributes.html
Normal file
14
netbox/templates/ui/panels/object_attributes.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for attr in attrs %}
|
||||
<tr>
|
||||
<th scope="row">{{ attr.label }}</th>
|
||||
<td>
|
||||
<div class="d-flex justify-content-between align-items-start">{{ attr.value }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
5
netbox/templates/ui/panels/objects_table.html
Normal file
5
netbox/templates/ui/panels/objects_table.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include 'builtins/htmx_table.html' %}
|
||||
{% endblock panel_content %}
|
||||
25
netbox/templates/ui/panels/related_objects.html
Normal file
25
netbox/templates/ui/panels/related_objects.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<ul class="list-group list-group-flush" role="presentation">
|
||||
{% for related_object_count in related_models %}
|
||||
{% action_url related_object_count.queryset.model 'list' as list_url %}
|
||||
{% if list_url %}
|
||||
<a href="{{ list_url }}?{{ related_object_count.filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ related_object_count.name }}
|
||||
{% with count=related_object_count.queryset.count %}
|
||||
{% if count %}
|
||||
<span class="badge text-bg-primary rounded-pill">{{ count }}</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-light rounded-pill">—</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<span class="list-group-item text-muted">{% trans "None" %}</span>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock panel_content %}
|
||||
@ -12,6 +12,7 @@ __all__ = (
|
||||
'flatten_dict',
|
||||
'ranges_to_string',
|
||||
'ranges_to_string_list',
|
||||
'resolve_attr_path',
|
||||
'shallow_compare_dict',
|
||||
'string_to_ranges',
|
||||
)
|
||||
@ -213,3 +214,26 @@ def string_to_ranges(value):
|
||||
return None
|
||||
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
|
||||
return values
|
||||
|
||||
|
||||
#
|
||||
# Attribute resolution
|
||||
#
|
||||
|
||||
def resolve_attr_path(obj, path):
|
||||
"""
|
||||
Follow a dotted path across attributes and/or dictionary keys and return the final value.
|
||||
|
||||
Parameters:
|
||||
obj: The starting object
|
||||
path: The dotted path to follow (e.g. "foo.bar.baz")
|
||||
"""
|
||||
cur = obj
|
||||
for part in path.split('.'):
|
||||
if cur is None:
|
||||
return None
|
||||
try:
|
||||
cur = getattr(cur, part) if hasattr(cur, part) else cur.get(part)
|
||||
except AttributeError:
|
||||
cur = None
|
||||
return cur
|
||||
|
||||
@ -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,11 @@ 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(context, component):
|
||||
"""
|
||||
Render a UI component (e.g. a Panel) by calling its render() method and passing the current template context.
|
||||
"""
|
||||
return mark_safe(component.render(context))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user