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'
|
- Getting Started: 'plugins/development/index.md'
|
||||||
- Models: 'plugins/development/models.md'
|
- Models: 'plugins/development/models.md'
|
||||||
- Views: 'plugins/development/views.md'
|
- Views: 'plugins/development/views.md'
|
||||||
|
- UI Components: 'plugins/development/ui-components.md'
|
||||||
- Navigation: 'plugins/development/navigation.md'
|
- Navigation: 'plugins/development/navigation.md'
|
||||||
- Templates: 'plugins/development/templates.md'
|
- Templates: 'plugins/development/templates.md'
|
||||||
- Tables: 'plugins/development/tables.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 import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
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 django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
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 extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
||||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||||
from netbox.object_actions import *
|
from netbox.object_actions import *
|
||||||
|
from netbox.ui import actions, layout
|
||||||
|
from netbox.ui.panels import (
|
||||||
|
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel,
|
||||||
|
TemplatePanel,
|
||||||
|
)
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
@ -221,6 +229,27 @@ class RegionListView(generic.ObjectListView):
|
|||||||
@register_model_view(Region)
|
@register_model_view(Region)
|
||||||
class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Region.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
regions = instance.get_descendants(include_self=True)
|
regions = instance.get_descendants(include_self=True)
|
||||||
@ -332,6 +361,27 @@ class SiteGroupListView(generic.ObjectListView):
|
|||||||
@register_model_view(SiteGroup)
|
@register_model_view(SiteGroup)
|
||||||
class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = SiteGroup.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
groups = instance.get_descendants(include_self=True)
|
groups = instance.get_descendants(include_self=True)
|
||||||
@ -461,6 +511,39 @@ class SiteListView(generic.ObjectListView):
|
|||||||
@register_model_view(Site)
|
@register_model_view(Site)
|
||||||
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Site.objects.prefetch_related('tenant__group')
|
queryset = Site.objects.prefetch_related('tenant__group')
|
||||||
|
layout = layout.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):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@ -561,6 +644,52 @@ class LocationListView(generic.ObjectListView):
|
|||||||
@register_model_view(Location)
|
@register_model_view(Location)
|
||||||
class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Location.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
locations = instance.get_descendants(include_self=True)
|
locations = instance.get_descendants(include_self=True)
|
||||||
@ -661,6 +790,16 @@ class RackRoleListView(generic.ObjectListView):
|
|||||||
@register_model_view(RackRole)
|
@register_model_view(RackRole)
|
||||||
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = RackRole.objects.all()
|
queryset = RackRole.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.RackRolePanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@ -727,7 +866,22 @@ class RackTypeListView(generic.ObjectListView):
|
|||||||
|
|
||||||
@register_model_view(RackType)
|
@register_model_view(RackType)
|
||||||
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
|
template_name = 'generic/object.html'
|
||||||
queryset = RackType.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@ -845,6 +999,22 @@ class RackElevationListView(generic.ObjectListView):
|
|||||||
@register_model_view(Rack)
|
@register_model_view(Rack)
|
||||||
class RackView(GetRelatedModelsMixin, generic.ObjectView):
|
class RackView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
|
||||||
@ -976,6 +1146,19 @@ class RackReservationListView(generic.ObjectListView):
|
|||||||
@register_model_view(RackReservation)
|
@register_model_view(RackReservation)
|
||||||
class RackReservationView(generic.ObjectView):
|
class RackReservationView(generic.ObjectView):
|
||||||
queryset = RackReservation.objects.all()
|
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)
|
@register_model_view(RackReservation, 'add', detail=False)
|
||||||
@ -1049,6 +1232,10 @@ class ManufacturerListView(generic.ObjectListView):
|
|||||||
@register_model_view(Manufacturer)
|
@register_model_view(Manufacturer)
|
||||||
class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
|
class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Manufacturer.objects.all()
|
queryset = Manufacturer.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[OrganizationalObjectPanel(), TagsPanel()],
|
||||||
|
right_panels=[RelatedObjectsPanel(), CustomFieldsPanel()],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@ -1122,6 +1309,18 @@ class DeviceTypeListView(generic.ObjectListView):
|
|||||||
@register_model_view(DeviceType)
|
@register_model_view(DeviceType)
|
||||||
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = DeviceType.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@ -1372,7 +1571,36 @@ class ModuleTypeProfileListView(generic.ObjectListView):
|
|||||||
|
|
||||||
@register_model_view(ModuleTypeProfile)
|
@register_model_view(ModuleTypeProfile)
|
||||||
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
|
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
|
template_name = 'generic/object.html'
|
||||||
queryset = ModuleTypeProfile.objects.all()
|
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)
|
@register_model_view(ModuleTypeProfile, 'add', detail=False)
|
||||||
@ -2213,6 +2441,43 @@ class DeviceListView(generic.ObjectListView):
|
|||||||
@register_model_view(Device)
|
@register_model_view(Device)
|
||||||
class DeviceView(generic.ObjectView):
|
class DeviceView(generic.ObjectView):
|
||||||
queryset = Device.objects.all()
|
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):
|
def get_extra_context(self, request, instance):
|
||||||
# VirtualChassis members
|
# VirtualChassis members
|
||||||
@ -2225,7 +2490,7 @@ class DeviceView(generic.ObjectView):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'vc_members': vc_members,
|
'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.
|
Note: If `template_name` is not specified, it will be determined automatically based on the queryset model.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
layout: An instance of `netbox.ui.layout.Layout` which defines the page layout (overrides HTML template)
|
||||||
tab: A ViewTab instance for the view
|
tab: A ViewTab instance for the view
|
||||||
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
actions: An iterable of ObjectAction subclasses (see ActionsMixin)
|
||||||
"""
|
"""
|
||||||
|
layout = None
|
||||||
tab = None
|
tab = None
|
||||||
actions = (CloneObject, EditObject, DeleteObject)
|
actions = (CloneObject, EditObject, DeleteObject)
|
||||||
|
|
||||||
@ -81,6 +83,7 @@ class ObjectView(ActionsMixin, BaseObjectView):
|
|||||||
'object': instance,
|
'object': instance,
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
'tab': self.tab,
|
'tab': self.tab,
|
||||||
|
'layout': self.layout,
|
||||||
**self.get_extra_context(request, instance),
|
**self.get_extra_context(request, instance),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,375 +1 @@
|
|||||||
{% extends 'dcim/device/base.html' %}
|
{% 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' %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -10,96 +6,3 @@
|
|||||||
<li class="breadcrumb-item">{{ location|linkify }}</li>
|
<li class="breadcrumb-item">{{ location|linkify }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block extra_controls %}
|
{% block extra_controls %}
|
||||||
@ -25,35 +22,3 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock extra_controls %}
|
{% 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' %}
|
{% 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' %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load buttons %}
|
|
||||||
{% load helpers %}
|
|
||||||
{% load static %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load mptt %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ 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"><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>
|
<li class="breadcrumb-item">{% trans "Units" %} {{ object.unit_list }}</li>
|
||||||
{% endblock %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block extra_controls %}
|
{% block extra_controls %}
|
||||||
@ -11,41 +8,3 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock extra_controls %}
|
{% 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' %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
@ -18,53 +15,3 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock extra_controls %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load tz %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load l10n %}
|
|
||||||
{% load mptt %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
{{ block.super }}
|
{{ 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>
|
<li class="breadcrumb-item"><a href="{% url 'dcim:site_list' %}?group_id={{ object.group.pk }}">{{ object.group }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% 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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load plugins %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
@ -18,53 +15,3 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock extra_controls %}
|
{% 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 %}
|
{% plugin_alerts object %}
|
||||||
{% endblock alerts %}
|
{% endblock alerts %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}
|
||||||
|
{# Render panel layout declared on view class #}
|
||||||
|
{% for row in layout.rows %}
|
||||||
|
<div class="row">
|
||||||
|
{% for column in row.columns %}
|
||||||
|
<div class="col">
|
||||||
|
{% for panel in column.panels %}
|
||||||
|
{% render panel %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block modals %}
|
{% block modals %}
|
||||||
{% include 'inc/htmx_modal.html' %}
|
{% 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',
|
'flatten_dict',
|
||||||
'ranges_to_string',
|
'ranges_to_string',
|
||||||
'ranges_to_string_list',
|
'ranges_to_string_list',
|
||||||
|
'resolve_attr_path',
|
||||||
'shallow_compare_dict',
|
'shallow_compare_dict',
|
||||||
'string_to_ranges',
|
'string_to_ranges',
|
||||||
)
|
)
|
||||||
@ -213,3 +214,26 @@ def string_to_ranges(value):
|
|||||||
return None
|
return None
|
||||||
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
|
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
|
||||||
return values
|
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 import template
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from utilities.querydict import dict_to_querydict
|
from utilities.querydict import dict_to_querydict
|
||||||
@ -179,3 +180,11 @@ def static_with_params(path, **params):
|
|||||||
# Reconstruct the URL with the new query string
|
# Reconstruct the URL with the new query string
|
||||||
new_parsed = parsed._replace(query=new_query)
|
new_parsed = parsed._replace(query=new_query)
|
||||||
return urlunparse(new_parsed)
|
return urlunparse(new_parsed)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def render(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