Compare commits

...

22 Commits

Author SHA1 Message Date
github-actions
bb73601d80 Update source translation strings 2026-03-27 05:31:05 +00:00
Arthur Hanson
99e9d96787 #20923: Migrate IPAM views to declarative layouts (#21695)
* #20923: Migrate IPAM views to declarative layouts

* #20923: Migrate IPAM views to declarative layouts

* fix VRF view

* fix Route Target view

* fix addressing details modal

* fix add prefix button

* fix add aggregate button

* fix add VLAN button

* fix breadcrumb on Application Service

* fix breadcrumb on ANS

* move attrs to separate file

* review feedback

* review feedback

* review feedback

* review feedback
2026-03-26 16:55:12 -04:00
bctiemann
f5c97e367c Merge pull request #21754 from netbox-community/20923-core-ui-layouts
#20923: Migrate core app to the new UI layouts
2026-03-26 13:53:20 -04:00
Arthur Hanson
ea756b29e9 #20923 - Convert tenancy to new UI layout (#21745) 2026-03-26 17:16:31 +01:00
Jeremy Stretch
b929e1aa1b Fixes #21747: Skip search caching when encountering an invalid schema during migrations (#21748) 2026-03-26 09:13:28 -07:00
github-actions
91d5382a61 Update source translation strings 2026-03-26 05:30:51 +00:00
Mark Robert Coleman
e76203238d Fix {module} placeholder resolution in module bay position field (#21752)
* Fix {module} placeholder resolution in module bay position field (#20467)

The {module} placeholder in ModuleBayTemplate's position field was not
being resolved when a module was installed, leaving the literal string
"{module}" in the position. This adds a resolve_position() method and
calls it in instantiate(), consistent with how resolve_name() and
resolve_label() already work.

Consolidates the shared resolution logic into _resolve_module_placeholder()
to eliminate duplication across resolve_name, resolve_label, and the new
resolve_position.

Fixes: #20467

* Move resolve_position() to ModuleBayTemplate

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-03-25 15:45:49 -04:00
Jeremy Stretch
3f58648115 Convert DataFileView to a single-column layout 2026-03-25 13:55:07 -04:00
Jeremy Stretch
b904dc5c75 Support translation of headings for embedded table panels 2026-03-25 13:50:41 -04:00
Jeremy Stretch
bf27ff9593 #20923: Initial work on migrating the core app 2026-03-25 12:57:10 -04:00
Martin Hauser
981f31304d Closes #21735: Replace deprecated Strawberry scalar for BigInt (#21736) 2026-03-25 09:36:30 -05:00
Martin Hauser
2a39ab47d6 feat(circuits): Add UI layout panels for circuits app
Implement comprehensive UI panel layouts for all circuit models using
the new panel system. Add panels for providers, circuits, terminations,
groups, and virtual circuits with proper attribute rendering and
actions.
2026-03-25 10:19:26 -04:00
Jeremy Stretch
aa01c16db0 #20923: Migrate remaining DCIM views to new UI layouts (#21706) 2026-03-25 09:08:54 -05:00
github-actions
e04986617c Update source translation strings 2026-03-25 05:28:00 +00:00
bctiemann
83cf193cdc Merge pull request #21680 from netbox-community/21664-update-github-actions-for-nodejs-24-compatibility
Closes #21664: Update and pin GitHub Actions for Node 24 compatibility
2026-03-24 14:34:57 -04:00
bctiemann
d497198f49 Merge pull request #21721 from netbox-community/21698-custom-field-url-filter-is-too-restrictive-for-weird-ports
Fixes #21698: Fix validation of custom field URLs with single-digit ports
2026-03-24 14:25:00 -04:00
pobradovic08
4e479c547f Closes #21480: Add 1.6T Ethernet interface types (#21723)
Add support for IEEE 802.3dj 1.6T fixed interface types and
published 1.6T pluggable form factors.

This adds 1.6TBASE-CR8, 1.6TBASE-KR8, 1.6TBASE-DR8, and
1.6TBASE-DR8-2, plus OSFP1600, OSFP1600-RHS, and QSFP-DD1600
transceiver types.
2026-03-24 10:51:26 +01:00
github-actions
e44c0a2119 Update source translation strings 2026-03-24 05:27:47 +00:00
Martin Hauser
3ab0613708 fix(circuits): Add ProviderAccount fieldsets (#21708) 2026-03-23 16:07:20 -07:00
Martin Hauser
9f16734266 fix(utilities): Allow single-digit port numbers in URL validator
Change port number regex from `\d{2,5}` to `\d{1,5}` to permit valid
single-digit ports (1-9). This aligns with RFC 3986 and fixes
validation for URLs using ports like :8 or :9.

Fixes #21698
2026-03-20 13:40:40 +01:00
Martin Hauser
268ef4f59f chore(ci): Pin CodeQL action to commit SHA
Pin GitHub/codeql-action references to full commit SHA v4.33.0 instead
of version tag to reduce supply chain risk from tag retargeting.
2026-03-16 15:14:23 +01:00
Martin Hauser
671b1cd470 chore(ci): Pin GitHub Actions to commit SHAs
Pin GitHub Actions references to full commit SHAs instead of version
tags to reduce supply chain risk from tag retargeting.

Update actions/checkout to v6.0.2, actions/setup-python to v6.2.0,
actions/setup-node to v6.3.0, actions/stale to v10.2.0, and
dessant/lock-threads to v6.0.0.
2026-03-16 14:35:51 +01:00
146 changed files with 4238 additions and 6025 deletions

View File

@@ -53,7 +53,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check Python linting & PEP8 compliance
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1
@@ -63,12 +63,12 @@ jobs:
src: "netbox/"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ matrix.node-version }}
@@ -76,7 +76,7 @@ jobs:
run: npm install -g yarn
- name: Setup Node.js with Yarn Caching
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ matrix.node-version }}
cache: yarn

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

View File

@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

View File

@@ -15,7 +15,7 @@ jobs:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
close-issue-message: >
This issue is being closed as no further information has been provided. If

View File

@@ -16,7 +16,7 @@ jobs:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
# General parameters
operations-per-run: 200

View File

@@ -27,16 +27,16 @@ jobs:
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -19,6 +19,6 @@ jobs:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6.0.0
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
discussion-inactive-days: 180

View File

@@ -27,12 +27,12 @@ jobs:
private-key: ${{ secrets.HOUSEKEEPING_SECRET_KEY }}
- name: Check out repo
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.12

View File

@@ -68,10 +68,14 @@ class ProviderAccountForm(PrimaryModelForm):
quick_add=True
)
fieldsets = (
FieldSet('provider', 'account', 'name', 'description', 'tags'),
)
class Meta:
model = ProviderAccount
fields = [
'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
'provider', 'account', 'name', 'description', 'owner', 'comments', 'tags',
]

View File

View File

@@ -0,0 +1,139 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, attrs, panels
from utilities.data import resolve_attr_path
class CircuitCircuitTerminationPanel(panels.ObjectPanel):
"""
A panel showing the CircuitTermination assigned to the object.
"""
template_name = 'circuits/panels/circuit_circuit_termination.html'
title = _('Termination')
def __init__(self, accessor=None, side=None, **kwargs):
super().__init__(**kwargs)
if accessor is not None:
self.accessor = accessor
if side is not None:
self.side = side
def get_context(self, context):
return {
**super().get_context(context),
'side': self.side,
'termination': resolve_attr_path(context, f'{self.accessor}.termination_{self.side.lower()}'),
}
class CircuitGroupAssignmentsPanel(panels.ObjectsTablePanel):
"""
A panel showing all Circuit Groups attached to the object.
"""
title = _('Group Assignments')
actions = [
actions.AddObject(
'circuits.CircuitGroupAssignment',
url_params={
'member_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'member': lambda ctx: ctx['object'].pk,
'return_url': lambda ctx: ctx['object'].get_absolute_url(),
},
label=_('Assign Group'),
),
]
def __init__(self, **kwargs):
super().__init__(
'circuits.CircuitGroupAssignment',
filters={
'member_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'member_id': lambda ctx: ctx['object'].pk,
},
**kwargs,
)
class CircuitGroupPanel(panels.OrganizationalObjectPanel):
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class CircuitGroupAssignmentPanel(panels.ObjectAttributesPanel):
group = attrs.RelatedObjectAttr('group', linkify=True)
provider = attrs.RelatedObjectAttr('member.provider', linkify=True)
member = attrs.GenericForeignKeyAttr('member', linkify=True)
priority = attrs.ChoiceAttr('priority')
class CircuitPanel(panels.ObjectAttributesPanel):
provider = attrs.RelatedObjectAttr('provider', linkify=True)
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
type = attrs.RelatedObjectAttr('type', linkify=True)
status = attrs.ChoiceAttr('status')
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
install_date = attrs.DateTimeAttr('install_date', spec='date')
termination_date = attrs.DateTimeAttr('termination_date', spec='date')
commit_rate = attrs.TemplatedAttr('commit_rate', template_name='circuits/circuit/attrs/commit_rate.html')
description = attrs.TextAttr('description')
class CircuitTypePanel(panels.OrganizationalObjectPanel):
color = attrs.ColorAttr('color')
class ProviderPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
asns = attrs.RelatedObjectListAttr('asns', linkify=True, label=_('ASNs'))
description = attrs.TextAttr('description')
class ProviderAccountPanel(panels.ObjectAttributesPanel):
provider = attrs.RelatedObjectAttr('provider', linkify=True)
account = attrs.TextAttr('account', style='font-monospace', copy_button=True)
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
class ProviderNetworkPanel(panels.ObjectAttributesPanel):
provider = attrs.RelatedObjectAttr('provider', linkify=True)
name = attrs.TextAttr('name')
service_id = attrs.TextAttr('service_id', label=_('Service ID'), style='font-monospace', copy_button=True)
description = attrs.TextAttr('description')
class VirtualCircuitTypePanel(panels.OrganizationalObjectPanel):
color = attrs.ColorAttr('color')
class VirtualCircuitPanel(panels.ObjectAttributesPanel):
provider = attrs.RelatedObjectAttr('provider', linkify=True)
provider_network = attrs.RelatedObjectAttr('provider_network', linkify=True)
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
type = attrs.RelatedObjectAttr('type', linkify=True)
status = attrs.ChoiceAttr('status')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class VirtualCircuitTerminationPanel(panels.ObjectAttributesPanel):
provider = attrs.RelatedObjectAttr('virtual_circuit.provider', linkify=True)
provider_network = attrs.RelatedObjectAttr('virtual_circuit.provider_network', linkify=True)
provider_account = attrs.RelatedObjectAttr('virtual_circuit.provider_account', linkify=True)
virtual_circuit = attrs.RelatedObjectAttr('virtual_circuit', linkify=True)
role = attrs.ChoiceAttr('role')
class VirtualCircuitTerminationInterfacePanel(panels.ObjectAttributesPanel):
title = _('Interface')
device = attrs.RelatedObjectAttr('interface.device', linkify=True)
interface = attrs.RelatedObjectAttr('interface', linkify=True)
type = attrs.ChoiceAttr('interface.type')
description = attrs.TextAttr('interface.description')

View File

@@ -1,13 +1,23 @@
from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
from ipam.models import ASN
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ObjectsTablePanel,
Panel,
RelatedObjectsPanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
from .ui import panels
#
# Providers
@@ -29,6 +39,35 @@ class ProviderListView(generic.ObjectListView):
@register_model_view(Provider)
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Provider.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ProviderPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='circuits.ProviderAccount',
filters={'provider_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'circuits.ProviderAccount', url_params={'provider': lambda ctx: ctx['object'].pk}
),
],
),
ObjectsTablePanel(
model='circuits.Circuit',
filters={'provider_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('circuits.Circuit', url_params={'provider': lambda ctx: ctx['object'].pk}),
],
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -44,7 +83,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
'provider_id',
),
),
),
),
}
@@ -108,6 +147,32 @@ class ProviderAccountListView(generic.ObjectListView):
@register_model_view(ProviderAccount)
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderAccount.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ProviderAccountPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='circuits.Circuit',
filters={'provider_account_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'circuits.Circuit',
url_params={
'provider': lambda ctx: ctx['object'].provider.pk,
'provider_account': lambda ctx: ctx['object'].pk,
},
),
],
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -174,6 +239,32 @@ class ProviderNetworkListView(generic.ObjectListView):
@register_model_view(ProviderNetwork)
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderNetwork.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ProviderNetworkPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='circuits.Circuit',
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
),
ObjectsTablePanel(
model='circuits.VirtualCircuit',
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'circuits.VirtualCircuit', url_params={'provider_network': lambda ctx: ctx['object'].pk}
),
],
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -251,6 +342,17 @@ class CircuitTypeListView(generic.ObjectListView):
@register_model_view(CircuitType)
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitType.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.CircuitTypePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -318,6 +420,20 @@ class CircuitListView(generic.ObjectListView):
@register_model_view(Circuit)
class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.CircuitPanel(),
panels.CircuitGroupAssignmentsPanel(),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.CircuitCircuitTerminationPanel(side='A'),
panels.CircuitCircuitTerminationPanel(side='Z'),
ImageAttachmentsPanel(),
],
)
@register_model_view(Circuit, 'add', detail=False)
@@ -390,6 +506,18 @@ class CircuitTerminationListView(generic.ObjectListView):
@register_model_view(CircuitTermination)
class CircuitTerminationView(generic.ObjectView):
queryset = CircuitTermination.objects.all()
layout = layout.SimpleLayout(
left_panels=[
Panel(
template_name='circuits/panels/circuit_termination.html',
title=_('Circuit Termination'),
)
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
],
)
@register_model_view(CircuitTermination, 'add', detail=False)
@@ -446,6 +574,17 @@ class CircuitGroupListView(generic.ObjectListView):
@register_model_view(CircuitGroup)
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitGroup.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.CircuitGroupPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -508,6 +647,15 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
@register_model_view(CircuitGroupAssignment)
class CircuitGroupAssignmentView(generic.ObjectView):
queryset = CircuitGroupAssignment.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.CircuitGroupAssignmentPanel(),
TagsPanel(),
],
right_panels=[
CustomFieldsPanel(),
],
)
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
@@ -560,6 +708,17 @@ class VirtualCircuitTypeListView(generic.ObjectListView):
@register_model_view(VirtualCircuitType)
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualCircuitType.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualCircuitTypePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -627,6 +786,30 @@ class VirtualCircuitListView(generic.ObjectListView):
@register_model_view(VirtualCircuit)
class VirtualCircuitView(generic.ObjectView):
queryset = VirtualCircuit.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualCircuitPanel(),
TagsPanel(),
],
right_panels=[
CustomFieldsPanel(),
CommentsPanel(),
panels.CircuitGroupAssignmentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='circuits.VirtualCircuitTermination',
title=_('Terminations'),
filters={'virtual_circuit_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject(
'circuits.VirtualCircuitTermination',
url_params={'virtual_circuit': lambda ctx: ctx['object'].pk},
),
],
),
],
)
@register_model_view(VirtualCircuit, 'add', detail=False)
@@ -698,6 +881,16 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
@register_model_view(VirtualCircuitTermination)
class VirtualCircuitTerminationView(generic.ObjectView):
queryset = VirtualCircuitTermination.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualCircuitTerminationPanel(),
TagsPanel(),
CustomFieldsPanel(),
],
right_panels=[
panels.VirtualCircuitTerminationInterfacePanel(),
],
)
@register_model_view(VirtualCircuitTermination, 'edit')

View File

91
netbox/core/ui/panels.py Normal file
View File

@@ -0,0 +1,91 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs, panels
class DataSourcePanel(panels.ObjectAttributesPanel):
title = _('Data Source')
name = attrs.TextAttr('name')
type = attrs.ChoiceAttr('type')
enabled = attrs.BooleanAttr('enabled')
status = attrs.ChoiceAttr('status')
sync_interval = attrs.ChoiceAttr('sync_interval', label=_('Sync interval'))
last_synced = attrs.DateTimeAttr('last_synced', label=_('Last synced'))
description = attrs.TextAttr('description')
source_url = attrs.TemplatedAttr(
'source_url',
label=_('URL'),
template_name='core/datasource/attrs/source_url.html',
)
ignore_rules = attrs.TemplatedAttr(
'ignore_rules',
label=_('Ignore rules'),
template_name='core/datasource/attrs/ignore_rules.html',
)
class DataSourceBackendPanel(panels.ObjectPanel):
template_name = 'core/panels/datasource_backend.html'
title = _('Backend')
class DataFilePanel(panels.ObjectAttributesPanel):
title = _('Data File')
source = attrs.RelatedObjectAttr('source', linkify=True)
path = attrs.TextAttr('path', style='font-monospace', copy_button=True)
last_updated = attrs.DateTimeAttr('last_updated')
size = attrs.TemplatedAttr('size', template_name='core/datafile/attrs/size.html')
hash = attrs.TextAttr('hash', label=_('SHA256 hash'), style='font-monospace', copy_button=True)
class DataFileContentPanel(panels.ObjectPanel):
template_name = 'core/panels/datafile_content.html'
title = _('Content')
class JobPanel(panels.ObjectAttributesPanel):
title = _('Job')
object_type = attrs.TemplatedAttr(
'object_type',
label=_('Object type'),
template_name='core/job/attrs/object_type.html',
)
name = attrs.TextAttr('name')
status = attrs.ChoiceAttr('status')
error = attrs.TextAttr('error')
user = attrs.TextAttr('user', label=_('Created by'))
class JobSchedulingPanel(panels.ObjectAttributesPanel):
title = _('Scheduling')
created = attrs.DateTimeAttr('created')
scheduled = attrs.TemplatedAttr('scheduled', template_name='core/job/attrs/scheduled.html')
started = attrs.DateTimeAttr('started')
completed = attrs.DateTimeAttr('completed')
queue = attrs.TextAttr('queue_name', label=_('Queue'))
class ObjectChangePanel(panels.ObjectAttributesPanel):
title = _('Change')
time = attrs.DateTimeAttr('time')
user = attrs.TemplatedAttr(
'user_name',
label=_('User'),
template_name='core/objectchange/attrs/user.html',
)
action = attrs.ChoiceAttr('action')
changed_object_type = attrs.TextAttr(
'changed_object_type',
label=_('Object type'),
)
changed_object = attrs.TemplatedAttr(
'object_repr',
label=_('Object'),
template_name='core/objectchange/attrs/changed_object.html',
)
message = attrs.TextAttr('message')
request_id = attrs.TemplatedAttr(
'request_id',
label=_('Request ID'),
template_name='core/objectchange/attrs/request_id.html',
)

View File

@@ -23,9 +23,20 @@ from rq.worker import Worker
from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.config import PARAMS, get_config
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.plugins.utils import get_installed_plugins
from netbox.ui import layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
PluginContentPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin
@@ -48,6 +59,7 @@ from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
from .ui import panels
#
# Data sources
@@ -67,6 +79,24 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource)
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DataSourcePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.DataSourceBackendPanel(),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='core.DataFile',
filters={'source_id': lambda ctx: ctx['object'].pk},
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -157,6 +187,14 @@ class DataFileListView(generic.ObjectListView):
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
actions = (DeleteObject,)
layout = layout.Layout(
layout.Row(
layout.Column(
panels.DataFilePanel(),
panels.DataFileContentPanel(),
),
),
)
@register_model_view(DataFile, 'delete')
@@ -188,6 +226,17 @@ class JobListView(generic.ObjectListView):
class JobView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
layout = layout.SimpleLayout(
left_panels=[
panels.JobPanel(),
],
right_panels=[
panels.JobSchedulingPanel(),
],
bottom_panels=[
JSONPanel('data', title=_('Data')),
],
)
@register_model_view(Job, 'log')
@@ -200,6 +249,13 @@ class JobLogView(generic.ObjectView):
badge=lambda obj: len(obj.log_entries),
weight=500,
)
layout = layout.Layout(
layout.Row(
layout.Column(
ContextTablePanel('table', title=_('Log Entries')),
),
),
)
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
@@ -241,6 +297,26 @@ class ObjectChangeListView(generic.ObjectListView):
@register_model_view(ObjectChange)
class ObjectChangeView(generic.ObjectView):
queryset = None
layout = layout.Layout(
layout.Row(
layout.Column(panels.ObjectChangePanel()),
layout.Column(TemplatePanel('core/panels/objectchange_difference.html')),
),
layout.Row(
layout.Column(TemplatePanel('core/panels/objectchange_prechange.html')),
layout.Column(TemplatePanel('core/panels/objectchange_postchange.html')),
),
layout.Row(
layout.Column(PluginContentPanel('left_page')),
layout.Column(PluginContentPanel('right_page')),
),
layout.Row(
layout.Column(
TemplatePanel('core/panels/objectchange_related.html'),
PluginContentPanel('full_width_page'),
),
),
)
def get_queryset(self, request):
return ObjectChange.objects.valid_models()
@@ -312,6 +388,14 @@ class ConfigRevisionListView(generic.ObjectListView):
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
TemplatePanel('core/panels/configrevision_data.html'),
TemplatePanel('core/panels/configrevision_comment.html'),
),
),
)
def get_extra_context(self, request, instance):
"""

View File

@@ -1003,6 +1003,11 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_800GE_SR8 = '800gbase-sr8'
TYPE_800GE_VR8 = '800gbase-vr8'
# 1.6 Tbps Ethernet
TYPE_1TE_CR8 = '1.6tbase-cr8'
TYPE_1TE_DR8 = '1.6tbase-dr8'
TYPE_1TE_DR8_2 = '1.6tbase-dr8-2'
# Ethernet (modular)
TYPE_100ME_SFP = '100base-x-sfp'
TYPE_1GE_GBIC = '1000base-x-gbic'
@@ -1036,6 +1041,9 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_400GE_CFP8 = '400gbase-x-cfp8'
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
TYPE_800GE_OSFP = '800gbase-x-osfp'
TYPE_1TE_OSFP1600 = '1.6tbase-x-osfp1600'
TYPE_1TE_OSFP1600_RHS = '1.6tbase-x-osfp1600-rhs'
TYPE_1TE_QSFP_DD1600 = '1.6tbase-x-qsfpdd1600'
# Backplane Ethernet
TYPE_1GE_KX = '1000base-kx'
@@ -1049,6 +1057,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100GE_KP4 = '100gbase-kp4'
TYPE_100GE_KR2 = '100gbase-kr2'
TYPE_100GE_KR4 = '100gbase-kr4'
TYPE_1TE_KR8 = '1.6tbase-kr8'
# Wireless
TYPE_80211A = 'ieee802.11a'
@@ -1298,6 +1307,14 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_800GE_VR8, '800GBASE-VR8 (800GE)'),
)
),
(
_('1.6 Tbps Ethernet'),
(
(TYPE_1TE_CR8, '1.6TBASE-CR8 (1.6TE)'),
(TYPE_1TE_DR8, '1.6TBASE-DR8 (1.6TE)'),
(TYPE_1TE_DR8_2, '1.6TBASE-DR8-2 (1.6TE)'),
)
),
(
_('Pluggable transceivers'),
(
@@ -1333,6 +1350,9 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_400GE_OSFP_RHS, 'OSFP-RHS (400GE)'),
(TYPE_800GE_OSFP, 'OSFP (800GE)'),
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
(TYPE_1TE_OSFP1600, 'OSFP1600 (1.6TE)'),
(TYPE_1TE_OSFP1600_RHS, 'OSFP1600-RHS (1.6TE)'),
(TYPE_1TE_QSFP_DD1600, 'QSFP-DD1600 (1.6TE)'),
)
),
(
@@ -1349,6 +1369,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100GE_KP4, '100GBASE-KP4 (100GE)'),
(TYPE_100GE_KR2, '100GBASE-KR2 (100GE)'),
(TYPE_100GE_KR4, '100GBASE-KR4 (100GE)'),
(TYPE_1TE_KR8, '1.6TBASE-KR8 (1.6TE)'),
)
),
(

View File

@@ -177,29 +177,19 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
modules.reverse()
return modules
def resolve_name(self, module):
if MODULE_TOKEN not in self.name:
return self.name
def _resolve_module_placeholder(self, value, module):
if MODULE_TOKEN not in value or not module:
return value
modules = self._get_module_tree(module)
for m in modules:
value = value.replace(MODULE_TOKEN, m.module_bay.position, 1)
return value
if module:
modules = self._get_module_tree(module)
name = self.name
for module in modules:
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
return name
return self.name
def resolve_name(self, module):
return self._resolve_module_placeholder(self.name, module)
def resolve_label(self, module):
if MODULE_TOKEN not in self.label:
return self.label
if module:
modules = self._get_module_tree(module)
label = self.label
for module in modules:
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
return label
return self.label
return self._resolve_module_placeholder(self.label, module)
class ConsolePortTemplate(ModularComponentTemplateModel):
@@ -729,11 +719,14 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
verbose_name = _('module bay template')
verbose_name_plural = _('module bay templates')
def resolve_position(self, module):
return self._resolve_module_placeholder(self.position, module)
def instantiate(self, **kwargs):
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
position=self.position,
position=self.resolve_position(kwargs.get('module')),
**kwargs
)
instantiate.do_not_call_in_templates = True

View File

@@ -849,6 +849,50 @@ class ModuleBayTestCase(TestCase):
nested_bay = module.modulebays.get(name='SFP A-21')
self.assertEqual(nested_bay.label, 'A-21')
@tag('regression') # #20467
def test_nested_module_bay_position_resolution(self):
"""Test that {module} in a module bay template's position field is resolved when the module is installed."""
manufacturer = Manufacturer.objects.first()
site = Site.objects.first()
device_role = DeviceRole.objects.first()
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Device with Position Test',
slug='device-with-position-test'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Slot 1',
position='1'
)
module_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='Module with Position Placeholder'
)
ModuleBayTemplate.objects.create(
module_type=module_type,
name='Sub-bay {module}-1',
position='{module}-1'
)
device = Device.objects.create(
name='Position Test Device',
device_type=device_type,
role=device_role,
site=site
)
module_bay = device.modulebays.get(name='Slot 1')
module = Module.objects.create(
device=device,
module_bay=module_bay,
module_type=module_type
)
nested_bay = module.modulebays.get(name='Sub-bay 1-1')
self.assertEqual(nested_bay.position, '1-1')
@tag('regression') # #20912
def test_module_bay_parent_cleared_when_module_removed(self):
"""Test that the parent field is properly cleared when a module bay's module assignment is removed"""

View File

@@ -1,6 +1,8 @@
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 attrs, panels
from netbox.ui import actions, attrs, panels
class SitePanel(panels.ObjectAttributesPanel):
@@ -189,16 +191,251 @@ class PlatformPanel(panels.NestedGroupObjectPanel):
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
class ConsolePortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
speed = attrs.ChoiceAttr('speed')
description = attrs.TextAttr('description')
class ConsoleServerPortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
speed = attrs.ChoiceAttr('speed')
description = attrs.TextAttr('description')
class PowerPortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
description = attrs.TextAttr('description')
maximum_draw = attrs.TextAttr('maximum_draw')
allocated_draw = attrs.TextAttr('allocated_draw')
class PowerOutletPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
color = attrs.ColorAttr('color')
power_port = attrs.RelatedObjectAttr('power_port', linkify=True)
feed_leg = attrs.ChoiceAttr('feed_leg')
class FrontPortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
color = attrs.ColorAttr('color')
positions = attrs.TextAttr('positions')
description = attrs.TextAttr('description')
class RearPortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
color = attrs.ColorAttr('color')
positions = attrs.TextAttr('positions')
description = attrs.TextAttr('description')
class ModuleBayPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
position = attrs.TextAttr('position')
description = attrs.TextAttr('description')
class DeviceBayPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
description = attrs.TextAttr('description')
class InventoryItemPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
parent = attrs.RelatedObjectAttr('parent', linkify=True, label=_('Parent item'))
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
status = attrs.ChoiceAttr('status')
role = attrs.RelatedObjectAttr('role', linkify=True)
component = attrs.GenericForeignKeyAttr('component', linkify=True)
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
part_id = attrs.TextAttr('part_id', label=_('Part ID'))
serial = attrs.TextAttr('serial')
asset_tag = attrs.TextAttr('asset_tag')
description = attrs.TextAttr('description')
class InventoryItemRolePanel(panels.OrganizationalObjectPanel):
color = attrs.ColorAttr('color')
class CablePanel(panels.ObjectAttributesPanel):
type = attrs.ChoiceAttr('type')
status = attrs.ChoiceAttr('status')
profile = attrs.ChoiceAttr('profile')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
label = attrs.TextAttr('label')
description = attrs.TextAttr('description')
color = attrs.ColorAttr('color')
length = attrs.NumericAttr('length', unit_accessor='get_length_unit_display')
class VirtualChassisPanel(panels.ObjectAttributesPanel):
domain = attrs.TextAttr('domain')
master = attrs.RelatedObjectAttr('master', linkify=True)
description = attrs.TextAttr('description')
class PowerPanelPanel(panels.ObjectAttributesPanel):
site = attrs.RelatedObjectAttr('site', linkify=True)
location = attrs.NestedObjectAttr('location', linkify=True)
description = attrs.TextAttr('description')
class PowerFeedPanel(panels.ObjectAttributesPanel):
power_panel = attrs.RelatedObjectAttr('power_panel', linkify=True)
rack = attrs.RelatedObjectAttr('rack', linkify=True)
type = attrs.ChoiceAttr('type')
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
connected_device = attrs.TemplatedAttr(
'connected_endpoints',
label=_('Connected device'),
template_name='dcim/powerfeed/attrs/connected_device.html',
)
utilization = attrs.TemplatedAttr(
'connected_endpoints',
label=_('Utilization (allocated)'),
template_name='dcim/powerfeed/attrs/utilization.html',
)
class PowerFeedElectricalPanel(panels.ObjectAttributesPanel):
title = _('Electrical Characteristics')
supply = attrs.ChoiceAttr('supply')
voltage = attrs.TextAttr('voltage', format_string=_('{}V'))
amperage = attrs.TextAttr('amperage', format_string=_('{}A'))
phase = attrs.ChoiceAttr('phase')
max_utilization = attrs.TextAttr('max_utilization', format_string='{}%')
class VirtualDeviceContextPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
device = attrs.RelatedObjectAttr('device', linkify=True)
identifier = attrs.TextAttr('identifier')
status = attrs.ChoiceAttr('status')
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',
)
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class MACAddressPanel(panels.ObjectAttributesPanel):
mac_address = attrs.TextAttr('mac_address', label=_('MAC address'), style='font-monospace', copy_button=True)
description = attrs.TextAttr('description')
assignment = attrs.RelatedObjectAttr('assigned_object', linkify=True, grouped_by='parent_object')
is_primary = attrs.BooleanAttr('is_primary', label=_('Primary for interface'))
class ConnectionPanel(panels.ObjectPanel):
"""
A panel which displays connection information for a cabled object.
"""
template_name = 'dcim/panels/connection.html'
title = _('Connection')
def __init__(self, trace_url_name, connect_options=None, show_endpoints=True, **kwargs):
super().__init__(**kwargs)
self.trace_url_name = trace_url_name
self.connect_options = connect_options or []
self.show_endpoints = show_endpoints
def get_context(self, context):
return {
**super().get_context(context),
'trace_url_name': self.trace_url_name,
'connect_options': self.connect_options,
'show_endpoints': self.show_endpoints,
}
def render(self, context):
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
class InventoryItemsPanel(panels.ObjectPanel):
"""
A panel which displays inventory items associated with a component.
"""
template_name = 'dcim/panels/component_inventory_items.html'
title = _('Inventory Items')
actions = [
actions.AddObject(
'dcim.inventoryitem',
url_params={
'component_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
'component_id': lambda ctx: ctx['object'].pk,
},
),
]
def render(self, context):
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
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')
actions = [
actions.AddObject(
'dcim.device',
url_params={
'site': lambda ctx: ctx['object'].master.site_id if ctx['object'].master else '',
'rack': lambda ctx: ctx['object'].master.rack_id if ctx['object'].master else '',
},
),
]
def get_context(self, context):
return {
**super().get_context(context),
'virtual_chassis': context.get('virtual_chassis'),
'vc_members': context.get('vc_members'),
}
@@ -226,3 +463,106 @@ class PowerUtilizationPanel(panels.ObjectPanel):
if not obj.powerports.exists() or not obj.poweroutlets.exists():
return ''
return super().render(context)
class InterfacePanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
speed = attrs.TemplatedAttr('speed', template_name='dcim/interface/attrs/speed.html', label=_('Speed'))
duplex = attrs.ChoiceAttr('duplex')
mtu = attrs.TextAttr('mtu', label=_('MTU'))
enabled = attrs.BooleanAttr('enabled')
mgmt_only = attrs.BooleanAttr('mgmt_only', label=_('Management only'))
description = attrs.TextAttr('description')
poe_mode = attrs.ChoiceAttr('poe_mode', label=_('PoE mode'))
poe_type = attrs.ChoiceAttr('poe_type', label=_('PoE type'))
mode = attrs.ChoiceAttr('mode', label=_('802.1Q mode'))
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
untagged_vlan = attrs.RelatedObjectAttr('untagged_vlan', linkify=True, label=_('Untagged VLAN'))
tx_power = attrs.TextAttr('tx_power', label=_('Transmit power (dBm)'))
tunnel = attrs.RelatedObjectAttr('tunnel_termination.tunnel', linkify=True, label=_('Tunnel'))
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
class RelatedInterfacesPanel(panels.ObjectAttributesPanel):
title = _('Related Interfaces')
parent = attrs.RelatedObjectAttr('parent', linkify=True)
bridge = attrs.RelatedObjectAttr('bridge', linkify=True)
lag = attrs.RelatedObjectAttr('lag', linkify=True, label=_('LAG'))
class InterfaceAddressingPanel(panels.ObjectAttributesPanel):
title = _('Addressing')
mac_address = attrs.TemplatedAttr(
'primary_mac_address',
template_name='dcim/interface/attrs/mac_address.html',
label=_('MAC address'),
)
wwn = attrs.TextAttr('wwn', style='font-monospace', label=_('WWN'))
vrf = attrs.RelatedObjectAttr('vrf', linkify=True, label=_('VRF'))
vlan_translation = attrs.RelatedObjectAttr('vlan_translation_policy', linkify=True, label=_('VLAN translation'))
class InterfaceConnectionPanel(panels.ObjectPanel):
"""
A connection panel for interfaces, which handles cable, wireless link, and virtual circuit cases.
"""
template_name = 'dcim/panels/interface_connection.html'
title = _('Connection')
def render(self, context):
obj = context.get('object')
if obj and obj.is_virtual:
return ''
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
class VirtualCircuitPanel(panels.ObjectPanel):
"""
A panel which displays virtual circuit information for a virtual interface.
"""
template_name = 'dcim/panels/interface_virtual_circuit.html'
title = _('Virtual Circuit')
def render(self, context):
obj = context.get('object')
if not obj or not obj.is_virtual or not obj.virtual_circuit_termination:
return ''
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
class InterfaceWirelessPanel(panels.ObjectPanel):
"""
A panel which displays wireless RF attributes for an interface, comparing local and peer values.
"""
template_name = 'dcim/panels/interface_wireless.html'
title = _('Wireless')
def render(self, context):
obj = context.get('object')
if not obj or not obj.is_wireless:
return ''
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))
class WirelessLANsPanel(panels.ObjectPanel):
"""
A panel which lists the wireless LANs associated with an interface.
"""
template_name = 'dcim/panels/interface_wireless_lans.html'
title = _('Wireless LANs')
def render(self, context):
obj = context.get('object')
if not obj or not obj.is_wireless:
return ''
ctx = self.get_context(context)
return render_to_string(self.template_name, ctx, request=ctx.get('request'))

View File

@@ -17,10 +17,12 @@ from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
from ipam.models import ASN, VLAN, IPAddress, Prefix, VLANGroup
from ipam.tables import VLANTranslationRuleTable
from ipam.ui.panels import FHRPGroupAssignmentsPanel
from netbox.object_actions import *
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
JSONPanel,
NestedGroupObjectPanel,
ObjectsTablePanel,
@@ -1577,7 +1579,7 @@ class ModuleTypeProfileListView(generic.ObjectListView):
@register_model_view(ModuleTypeProfile)
class ModuleTypeProfileView(GetRelatedModelsMixin, generic.ObjectView):
class ModuleTypeProfileView(generic.ObjectView):
template_name = 'generic/object.html'
queryset = ModuleTypeProfile.objects.all()
layout = layout.SimpleLayout(
@@ -2555,6 +2557,7 @@ class DeviceView(generic.ObjectView):
vc_members = []
return {
'virtual_chassis': instance.virtual_chassis,
'vc_members': vc_members,
'svg_extra': f'highlight=id:{instance.pk}',
}
@@ -2907,6 +2910,28 @@ class ConsolePortListView(generic.ObjectListView):
@register_model_view(ConsolePort)
class ConsolePortView(generic.ObjectView):
queryset = ConsolePort.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ConsolePortPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:consoleport_trace',
connect_options=[
{
'a_type': 'dcim.consoleport',
'b_type': 'dcim.consoleserverport',
'label': _('Console Server Port'),
},
{'a_type': 'dcim.consoleport', 'b_type': 'dcim.frontport', 'label': _('Front Port')},
{'a_type': 'dcim.consoleport', 'b_type': 'dcim.rearport', 'label': _('Rear Port')},
],
),
panels.InventoryItemsPanel(),
],
)
@register_model_view(ConsolePort, 'add', detail=False)
@@ -2978,6 +3003,24 @@ class ConsoleServerPortListView(generic.ObjectListView):
@register_model_view(ConsoleServerPort)
class ConsoleServerPortView(generic.ObjectView):
queryset = ConsoleServerPort.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ConsoleServerPortPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:consoleserverport_trace',
connect_options=[
{'a_type': 'dcim.consoleserverport', 'b_type': 'dcim.consoleport', 'label': _('Console Port')},
{'a_type': 'dcim.consoleserverport', 'b_type': 'dcim.frontport', 'label': _('Front Port')},
{'a_type': 'dcim.consoleserverport', 'b_type': 'dcim.rearport', 'label': _('Rear Port')},
],
),
panels.InventoryItemsPanel(),
],
)
@register_model_view(ConsoleServerPort, 'add', detail=False)
@@ -3049,6 +3092,23 @@ class PowerPortListView(generic.ObjectListView):
@register_model_view(PowerPort)
class PowerPortView(generic.ObjectView):
queryset = PowerPort.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PowerPortPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:powerport_trace',
connect_options=[
{'a_type': 'dcim.powerport', 'b_type': 'dcim.poweroutlet', 'label': _('Power Outlet')},
{'a_type': 'dcim.powerport', 'b_type': 'dcim.powerfeed', 'label': _('Power Feed')},
],
),
panels.InventoryItemsPanel(),
],
)
@register_model_view(PowerPort, 'add', detail=False)
@@ -3120,6 +3180,22 @@ class PowerOutletListView(generic.ObjectListView):
@register_model_view(PowerOutlet)
class PowerOutletView(generic.ObjectView):
queryset = PowerOutlet.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PowerOutletPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:poweroutlet_trace',
connect_options=[
{'a_type': 'dcim.poweroutlet', 'b_type': 'dcim.powerport', 'label': _('Power Port')},
],
),
panels.InventoryItemsPanel(),
],
)
@register_model_view(PowerOutlet, 'add', detail=False)
@@ -3191,6 +3267,45 @@ class InterfaceListView(generic.ObjectListView):
@register_model_view(Interface)
class InterfaceView(generic.ObjectView):
queryset = Interface.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.InterfacePanel(),
panels.RelatedInterfacesPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
ContextTablePanel('vdc_table', title=_('Virtual Device Contexts')),
panels.InterfaceAddressingPanel(),
panels.VirtualCircuitPanel(),
panels.InterfaceConnectionPanel(),
panels.InterfaceWirelessPanel(),
panels.WirelessLANsPanel(),
FHRPGroupAssignmentsPanel(),
panels.InventoryItemsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='ipam.IPAddress',
filters={'interface_id': lambda ctx: ctx['object'].pk},
title=_('IP Addresses'),
),
ObjectsTablePanel(
model='dcim.MACAddress',
filters={'interface_id': lambda ctx: ctx['object'].pk},
title=_('MAC Addresses'),
),
ObjectsTablePanel(
model='ipam.VLAN',
filters={'interface_id': lambda ctx: ctx['object'].pk},
title=_('VLANs'),
),
ContextTablePanel('lag_interfaces_table', title=_('LAG Members')),
ContextTablePanel('vlan_translation_table', title=_('VLAN Translation')),
ContextTablePanel('bridge_interfaces_table', title=_('Bridged Interfaces')),
ContextTablePanel('child_interfaces_table', title=_('Child Interfaces')),
],
)
def get_extra_context(self, request, instance):
# Get assigned VDCs
@@ -3205,30 +3320,29 @@ class InterfaceView(generic.ObjectView):
vdc_table.configure(request)
# Get bridge interfaces
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
bridge_interfaces_table = tables.InterfaceTable(
bridge_interfaces,
Interface.objects.restrict(request.user, 'view').filter(bridge=instance),
exclude=('device', 'parent'),
orderable=False
)
bridge_interfaces_table.configure(request)
# Get child interfaces
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
child_interfaces_table = tables.InterfaceTable(
child_interfaces,
Interface.objects.restrict(request.user, 'view').filter(parent=instance),
exclude=('device', 'parent'),
orderable=False
)
child_interfaces_table.configure(request)
# Get LAG interfaces
lag_interfaces = Interface.objects.restrict(request.user, 'view').filter(lag=instance)
lag_interfaces_table = tables.InterfaceLAGMemberTable(
lag_interfaces,
orderable=False
)
lag_interfaces_table.configure(request)
# Get LAG members (only for LAG interfaces)
lag_interfaces_table = None
if instance.is_lag:
lag_interfaces_table = tables.InterfaceLAGMemberTable(
Interface.objects.restrict(request.user, 'view').filter(lag=instance),
orderable=False
)
lag_interfaces_table.configure(request)
# Get VLAN translation rules
vlan_translation_table = None
@@ -3241,7 +3355,6 @@ class InterfaceView(generic.ObjectView):
return {
'vdc_table': vdc_table,
'bridge_interfaces': bridge_interfaces,
'bridge_interfaces_table': bridge_interfaces_table,
'child_interfaces_table': child_interfaces_table,
'lag_interfaces_table': lag_interfaces_table,
@@ -3329,6 +3442,33 @@ class FrontPortListView(generic.ObjectListView):
@register_model_view(FrontPort)
class FrontPortView(generic.ObjectView):
queryset = FrontPort.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.FrontPortPanel(),
CustomFieldsPanel(),
TagsPanel(),
panels.InventoryItemsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:frontport_trace',
show_endpoints=False,
connect_options=[
{'a_type': 'dcim.frontport', 'b_type': 'dcim.interface', 'label': _('Interface')},
{'a_type': 'dcim.frontport', 'b_type': 'dcim.consoleserverport', 'label': _('Console Server Port')},
{'a_type': 'dcim.frontport', 'b_type': 'dcim.consoleport', 'label': _('Console Port')},
{'a_type': 'dcim.frontport', 'b_type': 'dcim.frontport', 'label': _('Front Port')},
{'a_type': 'dcim.frontport', 'b_type': 'dcim.rearport', 'label': _('Rear Port')},
{
'a_type': 'dcim.frontport',
'b_type': 'circuits.circuittermination',
'label': _('Circuit Termination'),
},
],
),
TemplatePanel('dcim/panels/front_port_mappings.html'),
],
)
def get_extra_context(self, request, instance):
return {
@@ -3405,6 +3545,31 @@ class RearPortListView(generic.ObjectListView):
@register_model_view(RearPort)
class RearPortView(generic.ObjectView):
queryset = RearPort.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.RearPortPanel(),
CustomFieldsPanel(),
TagsPanel(),
panels.InventoryItemsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:rearport_trace',
show_endpoints=False,
connect_options=[
{'a_type': 'dcim.rearport', 'b_type': 'dcim.interface', 'label': _('Interface')},
{'a_type': 'dcim.rearport', 'b_type': 'dcim.frontport', 'label': _('Front Port')},
{'a_type': 'dcim.rearport', 'b_type': 'dcim.rearport', 'label': _('Rear Port')},
{
'a_type': 'dcim.rearport',
'b_type': 'circuits.circuittermination',
'label': _('Circuit Termination'),
},
],
),
TemplatePanel('dcim/panels/rear_port_mappings.html'),
],
)
def get_extra_context(self, request, instance):
return {
@@ -3481,6 +3646,19 @@ class ModuleBayListView(generic.ObjectListView):
@register_model_view(ModuleBay)
class ModuleBayView(generic.ObjectView):
queryset = ModuleBay.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ModuleBayPanel(),
TagsPanel(),
],
right_panels=[
CustomFieldsPanel(),
Panel(
title=_('Installed Module'),
template_name='dcim/panels/installed_module.html',
),
],
)
@register_model_view(ModuleBay, 'add', detail=False)
@@ -3543,6 +3721,19 @@ class DeviceBayListView(generic.ObjectListView):
@register_model_view(DeviceBay)
class DeviceBayView(generic.ObjectView):
queryset = DeviceBay.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DeviceBayPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
Panel(
title=_('Installed Device'),
template_name='dcim/panels/installed_device.html',
),
],
)
@register_model_view(DeviceBay, 'add', detail=False)
@@ -3686,6 +3877,13 @@ class InventoryItemListView(generic.ObjectListView):
@register_model_view(InventoryItem)
class InventoryItemView(generic.ObjectView):
queryset = InventoryItem.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.InventoryItemPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
)
@register_model_view(InventoryItem, 'edit')
@@ -3767,12 +3965,23 @@ class InventoryItemRoleListView(generic.ObjectListView):
@register_model_view(InventoryItemRole)
class InventoryItemRoleView(generic.ObjectView):
class InventoryItemRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = InventoryItemRole.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.InventoryItemRolePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
'inventoryitem_count': InventoryItem.objects.filter(role=instance).count(),
'related_models': self.get_related_models(request, instance),
}
@@ -3940,6 +4149,24 @@ class CableListView(generic.ObjectListView):
@register_model_view(Cable)
class CableView(generic.ObjectView):
queryset = Cable.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.CablePanel(),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
Panel(
title=_('Termination A'),
template_name='dcim/panels/cable_termination_a.html',
),
Panel(
title=_('Termination B'),
template_name='dcim/panels/cable_termination_b.html',
),
],
)
@register_model_view(Cable, 'add', detail=False)
@@ -4072,12 +4299,23 @@ class VirtualChassisListView(generic.ObjectListView):
@register_model_view(VirtualChassis)
class VirtualChassisView(generic.ObjectView):
queryset = VirtualChassis.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualChassisPanel(),
TagsPanel(),
CustomFieldsPanel(),
],
right_panels=[
panels.VirtualChassisMembersPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
vc_members = Device.objects.restrict(request.user).filter(virtual_chassis=instance).order_by('vc_position')
return {
'members': members,
'virtual_chassis': instance,
'vc_members': vc_members,
}
@@ -4317,6 +4555,27 @@ class PowerPanelListView(generic.ObjectListView):
@register_model_view(PowerPanel)
class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
queryset = PowerPanel.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PowerPanelPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
ImageAttachmentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.PowerFeed',
filters={'power_panel_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.PowerFeed', url_params={'power_panel': lambda ctx: ctx['object'].pk}),
],
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -4380,6 +4639,23 @@ class PowerFeedListView(generic.ObjectListView):
@register_model_view(PowerFeed)
class PowerFeedView(generic.ObjectView):
queryset = PowerFeed.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PowerFeedPanel(),
panels.PowerFeedElectricalPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.ConnectionPanel(
trace_url_name='dcim:powerfeed_trace',
connect_options=[
{'a_type': 'dcim.powerfeed', 'b_type': 'dcim.powerport', 'label': _('Power Port')},
],
),
CommentsPanel(),
],
)
@register_model_view(PowerFeed, 'add', detail=False)
@@ -4448,6 +4724,23 @@ class VirtualDeviceContextListView(generic.ObjectListView):
@register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VirtualDeviceContextPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.Interface',
filters={'vdc_id': lambda ctx: ctx['object'].pk},
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -4516,6 +4809,16 @@ class MACAddressListView(generic.ObjectListView):
@register_model_view(MACAddress)
class MACAddressView(generic.ObjectView):
queryset = MACAddress.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.MACAddressPanel(),
TagsPanel(),
CustomFieldsPanel(),
],
right_panels=[
CommentsPanel(),
],
)
@register_model_view(MACAddress, 'add', detail=False)

View File

@@ -367,6 +367,16 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
def get_status_color(self):
return PrefixStatusChoices.colors.get(self.status)
@cached_property
def aggregate(self):
"""
Return the containing Aggregate for this Prefix, if any.
"""
try:
return Aggregate.objects.get(prefix__net_contains_or_equals=str(self.prefix))
except Aggregate.DoesNotExist:
return None
def get_parents(self, include_self=False):
"""
Return all containing Prefixes in the hierarchy.

24
netbox/ipam/ui/attrs.py Normal file
View File

@@ -0,0 +1,24 @@
from django.template.loader import render_to_string
from netbox.ui import attrs
class VRFDisplayAttr(attrs.ObjectAttribute):
"""
Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
the route distinguisher (RD).
"""
template_name = 'ipam/attrs/vrf.html'
def __init__(self, *args, show_rd=False, **kwargs):
super().__init__(*args, **kwargs)
self.show_rd = show_rd
def render(self, obj, context):
value = self.get_value(obj)
return render_to_string(self.template_name, {
**self.get_context(obj, context),
'name': context['name'],
'value': value,
'show_rd': self.show_rd,
})

View File

@@ -2,14 +2,15 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, panels
from netbox.ui import actions, attrs, panels
from .attrs import VRFDisplayAttr
class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
"""
A panel which lists all FHRP group assignments for a given object.
"""
template_name = 'ipam/panels/fhrp_groups.html'
title = _('FHRP Groups')
actions = [
@@ -35,3 +36,220 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
label=_('Assign Group'),
),
]
class VRFPanel(panels.ObjectAttributesPanel):
rd = attrs.TextAttr('rd', label=_('Route Distinguisher'), style='font-monospace')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
enforce_unique = attrs.BooleanAttr('enforce_unique', label=_('Unique IP Space'))
description = attrs.TextAttr('description')
class RouteTargetPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name', style='font-monospace')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class RIRPanel(panels.OrganizationalObjectPanel):
is_private = attrs.BooleanAttr('is_private', label=_('Private'))
class ASNRangePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
range = attrs.TextAttr('range_as_string_with_asdot', label=_('Range'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class ASNPanel(panels.ObjectAttributesPanel):
asn = attrs.TextAttr('asn_with_asdot', label=_('AS Number'))
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class AggregatePanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
utilization = attrs.TemplatedAttr(
'prefix',
template_name='ipam/aggregate/attrs/utilization.html',
label=_('Utilization'),
)
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
date_added = attrs.DateTimeAttr('date_added', spec='date', label=_('Date Added'))
description = attrs.TextAttr('description')
class RolePanel(panels.OrganizationalObjectPanel):
weight = attrs.NumericAttr('weight')
class IPRangePanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
start_address = attrs.TextAttr('start_address', label=_('Starting Address'))
end_address = attrs.TextAttr('end_address', label=_('Ending Address'))
size = attrs.NumericAttr('size')
mark_populated = attrs.BooleanAttr('mark_populated', label=_('Marked Populated'))
mark_utilized = attrs.BooleanAttr('mark_utilized', label=_('Marked Utilized'))
utilization = attrs.TemplatedAttr(
'utilization',
template_name='ipam/iprange/attrs/utilization.html',
label=_('Utilization'),
)
vrf = VRFDisplayAttr('vrf', label=_('VRF'), show_rd=True)
role = attrs.RelatedObjectAttr('role', linkify=True)
status = attrs.ChoiceAttr('status')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class IPAddressPanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
vrf = VRFDisplayAttr('vrf', label=_('VRF'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.ChoiceAttr('role')
dns_name = attrs.TextAttr('dns_name', label=_('DNS Name'))
description = attrs.TextAttr('description')
assigned_object = attrs.RelatedObjectAttr(
'assigned_object',
linkify=True,
grouped_by='parent_object',
label=_('Assignment'),
)
nat_inside = attrs.TemplatedAttr(
'nat_inside',
template_name='ipam/ipaddress/attrs/nat_inside.html',
label=_('NAT (inside)'),
)
nat_outside = attrs.TemplatedAttr(
'nat_outside',
template_name='ipam/ipaddress/attrs/nat_outside.html',
label=_('NAT (outside)'),
)
is_primary_ip = attrs.BooleanAttr('is_primary_ip', label=_('Primary IP'))
is_oob_ip = attrs.BooleanAttr('is_oob_ip', label=_('OOB IP'))
class PrefixPanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
vrf = VRFDisplayAttr('vrf', label=_('VRF'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
aggregate = attrs.TemplatedAttr(
'aggregate',
template_name='ipam/prefix/attrs/aggregate.html',
label=_('Aggregate'),
)
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
vlan = attrs.RelatedObjectAttr('vlan', linkify=True, label=_('VLAN'), grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.RelatedObjectAttr('role', linkify=True)
description = attrs.TextAttr('description')
is_pool = attrs.BooleanAttr('is_pool', label=_('Is a pool'))
class VLANGroupPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
vid_ranges = attrs.TemplatedAttr(
'vid_ranges_items',
template_name='ipam/vlangroup/attrs/vid_ranges.html',
label=_('VLAN IDs'),
)
utilization = attrs.UtilizationAttr('utilization')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class VLANTranslationPolicyPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
class VLANTranslationRulePanel(panels.ObjectAttributesPanel):
policy = attrs.RelatedObjectAttr('policy', linkify=True)
local_vid = attrs.NumericAttr('local_vid', label=_('Local VID'))
remote_vid = attrs.NumericAttr('remote_vid', label=_('Remote VID'))
description = attrs.TextAttr('description')
class FHRPGroupPanel(panels.ObjectAttributesPanel):
protocol = attrs.ChoiceAttr('protocol')
group_id = attrs.NumericAttr('group_id', label=_('Group ID'))
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
member_count = attrs.NumericAttr('member_count', label=_('Members'))
class FHRPGroupAuthPanel(panels.ObjectAttributesPanel):
title = _('Authentication')
auth_type = attrs.ChoiceAttr('auth_type', label=_('Authentication Type'))
auth_key = attrs.TextAttr('auth_key', label=_('Authentication Key'))
class VLANPanel(panels.ObjectAttributesPanel):
region = attrs.NestedObjectAttr('site.region', linkify=True, label=_('Region'))
site = attrs.RelatedObjectAttr('site', linkify=True)
group = attrs.RelatedObjectAttr('group', linkify=True)
vid = attrs.NumericAttr('vid', label=_('VLAN ID'))
name = attrs.TextAttr('name')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.RelatedObjectAttr('role', linkify=True)
description = attrs.TextAttr('description')
qinq_role = attrs.ChoiceAttr('qinq_role', label=_('Q-in-Q Role'))
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
"""
A panel listing customer VLANs (C-VLANs) for an S-VLAN. Only renders when the VLAN has Q-in-Q
role 'svlan'.
"""
def __init__(self):
super().__init__(
'ipam.vlan',
filters={'qinq_svlan_id': lambda ctx: ctx['object'].pk},
title=_('Customer VLANs'),
actions=[
actions.AddObject(
'ipam.vlan',
url_params={
'qinq_role': 'cvlan',
'qinq_svlan': lambda ctx: ctx['object'].pk,
},
label=_('Add a VLAN'),
),
],
)
def render(self, context):
obj = context.get('object')
if not obj or obj.qinq_role != 'svlan':
return ''
return super().render(context)
class ServiceTemplatePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
protocol = attrs.ChoiceAttr('protocol')
ports = attrs.TextAttr('port_list', label=_('Ports'))
description = attrs.TextAttr('description')
class ServicePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
parent = attrs.RelatedObjectAttr('parent', linkify=True)
protocol = attrs.ChoiceAttr('protocol')
ports = attrs.TextAttr('port_list', label=_('Ports'))
ip_addresses = attrs.TemplatedAttr(
'ipaddresses',
template_name='ipam/service/attrs/ip_addresses.html',
label=_('IP Addresses'),
)
description = attrs.TextAttr('description')

View File

@@ -9,8 +9,16 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm
from dcim.models import Device, Interface, Site
from ipam.tables import VLANTranslationRuleTable
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
ObjectsTablePanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering
@@ -23,6 +31,7 @@ from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
from .ui import panels
from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
#
@@ -41,6 +50,27 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF)
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
panels.VRFPanel(),
TagsPanel(),
),
layout.Column(
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
),
),
layout.Row(
layout.Column(
ContextTablePanel('import_targets_table', title=_('Import route targets')),
),
layout.Column(
ContextTablePanel('export_targets_table', title=_('Export route targets')),
),
),
)
def get_extra_context(self, request, instance):
import_targets_table = tables.RouteTargetTable(
@@ -134,6 +164,50 @@ class RouteTargetListView(generic.ObjectListView):
@register_model_view(RouteTarget)
class RouteTargetView(generic.ObjectView):
queryset = RouteTarget.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
panels.RouteTargetPanel(),
TagsPanel(),
),
layout.Column(
CustomFieldsPanel(),
CommentsPanel(),
),
),
layout.Row(
layout.Column(
ObjectsTablePanel(
'ipam.vrf',
filters={'import_target_id': lambda ctx: ctx['object'].pk},
title=_('Importing VRFs'),
),
),
layout.Column(
ObjectsTablePanel(
'ipam.vrf',
filters={'export_target_id': lambda ctx: ctx['object'].pk},
title=_('Exporting VRFs'),
),
),
),
layout.Row(
layout.Column(
ObjectsTablePanel(
'vpn.l2vpn',
filters={'import_target_id': lambda ctx: ctx['object'].pk},
title=_('Importing L2VPNs'),
),
),
layout.Column(
ObjectsTablePanel(
'vpn.l2vpn',
filters={'export_target_id': lambda ctx: ctx['object'].pk},
title=_('Exporting L2VPNs'),
),
),
),
)
@register_model_view(RouteTarget, 'add', detail=False)
@@ -192,6 +266,17 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR)
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.RIRPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -257,6 +342,16 @@ class ASNRangeListView(generic.ObjectListView):
@register_model_view(ASNRange)
class ASNRangeView(generic.ObjectView):
queryset = ASNRange.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ASNRangePanel(),
TagsPanel(),
],
right_panels=[
CommentsPanel(),
CustomFieldsPanel(),
],
)
@register_model_view(ASNRange, 'asns')
@@ -337,6 +432,17 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN)
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ASNPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -412,6 +518,16 @@ class AggregateListView(generic.ObjectListView):
@register_model_view(Aggregate)
class AggregateView(generic.ObjectView):
queryset = Aggregate.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.AggregatePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
@register_model_view(Aggregate, 'prefixes')
@@ -506,6 +622,17 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role)
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.RolePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -569,15 +696,23 @@ class PrefixListView(generic.ObjectListView):
@register_model_view(Prefix)
class PrefixView(generic.ObjectView):
queryset = Prefix.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PrefixPanel(),
],
right_panels=[
TemplatePanel('ipam/panels/prefix_addressing.html'),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
bottom_panels=[
ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
],
)
def get_extra_context(self, request, instance):
try:
aggregate = Aggregate.objects.restrict(request.user, 'view').get(
prefix__net_contains_or_equals=str(instance.prefix)
)
except Aggregate.DoesNotExist:
aggregate = None
# Parent prefixes table
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
@@ -608,11 +743,12 @@ class PrefixView(generic.ObjectView):
)
duplicate_prefix_table.configure(request)
return {
'aggregate': aggregate,
context = {
'parent_prefix_table': parent_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
}
if duplicate_prefixes.exists():
context['duplicate_prefix_table'] = duplicate_prefix_table
return context
@register_model_view(Prefix, 'prefixes')
@@ -756,6 +892,19 @@ class IPRangeListView(generic.ObjectListView):
@register_model_view(IPRange)
class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IPRangePanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
],
)
def get_extra_context(self, request, instance):
@@ -853,6 +1002,23 @@ class IPAddressListView(generic.ObjectListView):
@register_model_view(IPAddress)
class IPAddressView(generic.ObjectView):
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
layout = layout.SimpleLayout(
left_panels=[
panels.IPAddressPanel(),
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
right_panels=[
ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
ObjectsTablePanel(
'ipam.service',
filters={'ip_address_id': lambda ctx: ctx['object'].pk},
title=_('Application services'),
),
],
)
def get_extra_context(self, request, instance):
# Parent prefixes table
@@ -885,10 +1051,12 @@ class IPAddressView(generic.ObjectView):
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
duplicate_ips_table.configure(request)
return {
context = {
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
}
if duplicate_ips.exists():
context['duplicate_ips_table'] = duplicate_ips_table
return context
@register_model_view(IPAddress, 'add', detail=False)
@@ -1038,6 +1206,17 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup)
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization()
layout = layout.SimpleLayout(
left_panels=[
panels.VLANGroupPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -1125,19 +1304,32 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
@register_model_view(VLANTranslationPolicy)
class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
class VLANTranslationPolicyView(generic.ObjectView):
queryset = VLANTranslationPolicy.objects.all()
def get_extra_context(self, request, instance):
vlan_translation_table = VLANTranslationRuleTable(
data=instance.rules.all(),
orderable=False
)
vlan_translation_table.configure(request)
return {
'vlan_translation_table': vlan_translation_table,
}
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationPolicyPanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.vlantranslationrule',
filters={'policy_id': lambda ctx: ctx['object'].pk},
title=_('VLAN translation rules'),
actions=[
actions.AddObject(
'ipam.vlantranslationrule',
url_params={'policy': lambda ctx: ctx['object'].pk},
label=_('Add Rule'),
),
],
),
],
)
@register_model_view(VLANTranslationPolicy, 'add', detail=False)
@@ -1193,13 +1385,17 @@ class VLANTranslationRuleListView(generic.ObjectListView):
@register_model_view(VLANTranslationRule)
class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
class VLANTranslationRuleView(generic.ObjectView):
queryset = VLANTranslationRule.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationRulePanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
],
)
@register_model_view(VLANTranslationRule, 'add', detail=False)
@@ -1251,7 +1447,36 @@ class FHRPGroupListView(generic.ObjectListView):
@register_model_view(FHRPGroup)
class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = FHRPGroup.objects.all()
queryset = FHRPGroup.objects.annotate(
member_count=count_related(FHRPGroupAssignment, 'group')
)
layout = layout.SimpleLayout(
left_panels=[
panels.FHRPGroupPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.FHRPGroupAuthPanel(),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.ipaddress',
filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
title=_('Virtual IP addresses'),
actions=[
actions.AddObject(
'ipam.ipaddress',
url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
label=_('Add IP Address'),
),
],
),
ContextTablePanel('members_table', title=_('Members')),
],
)
def get_extra_context(self, request, instance):
# Get assigned interfaces
@@ -1276,7 +1501,6 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
),
),
'members_table': members_table,
'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
}
@@ -1379,17 +1603,35 @@ class VLANListView(generic.ObjectListView):
@register_model_view(VLAN)
class VLANView(generic.ObjectView):
queryset = VLAN.objects.all()
def get_extra_context(self, request, instance):
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
'vrf', 'scope', 'role', 'tenant'
)
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
prefix_table.configure(request)
return {
'prefix_table': prefix_table,
}
layout = layout.SimpleLayout(
left_panels=[
panels.VLANPanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.prefix',
filters={'vlan_id': lambda ctx: ctx['object'].pk},
title=_('Prefixes'),
actions=[
actions.AddObject(
'ipam.prefix',
url_params={
'tenant': lambda ctx: ctx['object'].tenant.pk if ctx['object'].tenant else None,
'site': lambda ctx: ctx['object'].site.pk if ctx['object'].site else None,
'vlan': lambda ctx: ctx['object'].pk,
},
label=_('Add a Prefix'),
),
],
),
panels.VLANCustomerVLANsPanel(),
],
)
@register_model_view(VLAN, 'interfaces')
@@ -1483,6 +1725,16 @@ class ServiceTemplateListView(generic.ObjectListView):
@register_model_view(ServiceTemplate)
class ServiceTemplateView(generic.ObjectView):
queryset = ServiceTemplate.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ServiceTemplatePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
@register_model_view(ServiceTemplate, 'add', detail=False)
@@ -1539,6 +1791,16 @@ class ServiceListView(generic.ObjectListView):
@register_model_view(Service)
class ServiceView(generic.ObjectView):
queryset = Service.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ServicePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
context = {}

View File

@@ -1,10 +1,12 @@
from typing import Union
from typing import NewType
import strawberry
BigInt = strawberry.scalar(
Union[int, str], # type: ignore
BigInt = NewType('BigInt', int)
BigIntScalar = strawberry.scalar(
name='BigInt',
serialize=lambda v: int(v),
parse_value=lambda v: str(v),
description="BigInt field",
description='BigInt field',
)

View File

@@ -16,6 +16,8 @@ from virtualization.graphql.schema import VirtualizationQuery
from vpn.graphql.schema import VPNQuery
from wireless.graphql.schema import WirelessQuery
from .scalars import BigInt, BigIntScalar
@strawberry.type
class Query(
@@ -36,9 +38,14 @@ class Query(
schema = strawberry.Schema(
query=Query,
config=StrawberryConfig(auto_camel_case=False),
config=StrawberryConfig(
auto_camel_case=False,
scalar_map={
BigInt: BigIntScalar,
},
),
extensions=[
DjangoOptimizerExtension(prefetch_custom_queryset=True),
MaxAliasesLimiter(max_alias_count=settings.GRAPHQL_MAX_ALIASES),
]
],
)

View File

@@ -1,9 +1,11 @@
import logging
from collections import defaultdict
import netaddr
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.db import ProgrammingError
from django.db.models import F, Q, Window, prefetch_related_objects
from django.db.models.fields.related import ForeignKey
from django.db.models.functions import window
@@ -24,6 +26,8 @@ from . import FieldTypes, LookupTypes, get_indexer
DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL
MAX_RESULTS = 1000
logger = logging.getLogger(__name__)
class SearchBackend:
"""
@@ -63,7 +67,12 @@ class SearchBackend:
"""
Receiver for the post_save signal, responsible for caching object creation/changes.
"""
self.cache(instance, remove_existing=not created)
try:
self.cache(instance, remove_existing=not created)
except ProgrammingError as e:
# The schema may be incomplete during migrations; skip caching.
logger.warning(f"Skipping search cache update due to schema error: {e}")
pass
def removal_handler(self, sender, instance, **kwargs):
"""

View File

@@ -0,0 +1,215 @@
from django.test import TestCase
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.models import (
Provider,
ProviderNetwork,
VirtualCircuit,
VirtualCircuitTermination,
VirtualCircuitType,
)
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from netbox.ui import attrs
from utilities.testing import create_test_device
from vpn.choices import (
AuthenticationAlgorithmChoices,
AuthenticationMethodChoices,
DHGroupChoices,
EncryptionAlgorithmChoices,
IKEModeChoices,
IKEVersionChoices,
IPSecModeChoices,
)
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile
class ChoiceAttrTest(TestCase):
"""
Test class for validating the behavior of ChoiceAttr attribute accessor.
This test class verifies that the ChoiceAttr class correctly handles
choice field attributes on Django model instances, including both direct
field access and related object field access. It tests the retrieval of
display values and associated context information such as color values
for choice fields. The test data includes a network topology with devices,
interfaces, providers, and virtual circuits to cover various scenarios of
choice field access patterns.
"""
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
interface = Interface.objects.create(
device=device,
name='vlan.100',
type=InterfaceTypeChoices.TYPE_VIRTUAL,
)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(
provider=provider,
name='Provider Network 1',
)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1',
)
virtual_circuit = VirtualCircuit.objects.create(
cid='VC-100',
provider_network=provider_network,
type=virtual_circuit_type,
status=CircuitStatusChoices.STATUS_ACTIVE,
)
cls.termination = VirtualCircuitTermination.objects.create(
virtual_circuit=virtual_circuit,
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=interface,
)
def test_choice_attr_direct_accessor(self):
attr = attrs.ChoiceAttr('role')
self.assertEqual(
attr.get_value(self.termination),
self.termination.get_role_display(),
)
self.assertEqual(
attr.get_context(self.termination, {}),
{'bg_color': self.termination.get_role_color()},
)
def test_choice_attr_related_accessor(self):
attr = attrs.ChoiceAttr('interface.type')
self.assertEqual(
attr.get_value(self.termination),
self.termination.interface.get_type_display(),
)
self.assertEqual(
attr.get_context(self.termination, {}),
{'bg_color': None},
)
def test_choice_attr_related_accessor_with_color(self):
attr = attrs.ChoiceAttr('virtual_circuit.status')
self.assertEqual(
attr.get_value(self.termination),
self.termination.virtual_circuit.get_status_display(),
)
self.assertEqual(
attr.get_context(self.termination, {}),
{'bg_color': self.termination.virtual_circuit.get_status_color()},
)
class RelatedObjectListAttrTest(TestCase):
"""
Test suite for RelatedObjectListAttr functionality.
This test class validates the behavior of the RelatedObjectListAttr class,
which is used to render related objects as HTML lists. It tests various
scenarios including direct accessor access, related accessor access through
foreign keys, empty related object sets, and rendering with maximum item
limits and overflow indicators. The tests use IKE and IPSec VPN policy
models to verify proper rendering of one-to-many and many-to-many
relationships between objects.
"""
@classmethod
def setUpTestData(cls):
cls.proposals = (
IKEProposal.objects.create(
name='IKE Proposal 1',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14,
),
IKEProposal.objects.create(
name='IKE Proposal 2',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14,
),
IKEProposal.objects.create(
name='IKE Proposal 3',
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
group=DHGroupChoices.GROUP_14,
),
)
cls.ike_policy = IKEPolicy.objects.create(
name='IKE Policy 1',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
)
cls.ike_policy.proposals.set(cls.proposals)
cls.empty_ike_policy = IKEPolicy.objects.create(
name='IKE Policy 2',
version=IKEVersionChoices.VERSION_1,
mode=IKEModeChoices.MAIN,
)
cls.ipsec_policy = IPSecPolicy.objects.create(name='IPSec Policy 1')
cls.profile = IPSecProfile.objects.create(
name='IPSec Profile 1',
mode=IPSecModeChoices.ESP,
ike_policy=cls.ike_policy,
ipsec_policy=cls.ipsec_policy,
)
cls.empty_profile = IPSecProfile.objects.create(
name='IPSec Profile 2',
mode=IPSecModeChoices.ESP,
ike_policy=cls.empty_ike_policy,
ipsec_policy=cls.ipsec_policy,
)
def test_related_object_list_attr_direct_accessor(self):
attr = attrs.RelatedObjectListAttr('proposals', linkify=False)
rendered = attr.render(self.ike_policy, {'name': 'proposals'})
self.assertIn('list-unstyled mb-0', rendered)
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
self.assertInHTML('<li>IKE Proposal 3</li>', rendered)
self.assertEqual(rendered.count('<li'), 3)
def test_related_object_list_attr_related_accessor(self):
attr = attrs.RelatedObjectListAttr('ike_policy.proposals', linkify=False)
rendered = attr.render(self.profile, {'name': 'proposals'})
self.assertIn('list-unstyled mb-0', rendered)
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
self.assertInHTML('<li>IKE Proposal 3</li>', rendered)
self.assertEqual(rendered.count('<li'), 3)
def test_related_object_list_attr_empty_related_accessor(self):
attr = attrs.RelatedObjectListAttr('ike_policy.proposals', linkify=False)
self.assertEqual(
attr.render(self.empty_profile, {'name': 'proposals'}),
attr.placeholder,
)
def test_related_object_list_attr_max_items(self):
attr = attrs.RelatedObjectListAttr(
'ike_policy.proposals',
linkify=False,
max_items=2,
overflow_indicator='',
)
rendered = attr.render(self.profile, {'name': 'proposals'})
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
self.assertNotIn('IKE Proposal 3', rendered)
self.assertIn('', rendered)

View File

@@ -18,6 +18,7 @@ __all__ = (
'NumericAttr',
'ObjectAttribute',
'RelatedObjectAttr',
'RelatedObjectListAttr',
'TemplatedAttr',
'TextAttr',
'TimezoneAttr',
@@ -145,22 +146,40 @@ 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.
The class calls get_FOO_display() on the terminal object resolved by the accessor
to retrieve the human-friendly choice label. For example, accessor="interface.type"
will call interface.get_type_display().
If a get_FOO_color() method exists on that object, it will be used to render a
background color for the attribute value.
"""
template_name = 'ui/attrs/choice.html'
def _resolve_target(self, obj):
if not self.accessor or '.' not in self.accessor:
return obj, self.accessor
object_accessor, field_name = self.accessor.rsplit('.', 1)
return resolve_attr_path(obj, object_accessor), field_name
def get_value(self, obj):
try:
return getattr(obj, f'get_{self.accessor}_display')()
except AttributeError:
return resolve_attr_path(obj, self.accessor)
target, field_name = self._resolve_target(obj)
if target is None:
return None
display = getattr(target, f'get_{field_name}_display', None)
if callable(display):
return display()
return resolve_attr_path(target, field_name)
def get_context(self, obj, context):
try:
bg_color = getattr(obj, f'get_{self.accessor}_color')()
except AttributeError:
bg_color = None
target, field_name = self._resolve_target(obj)
if target is None:
return {'bg_color': None}
get_color = getattr(target, f'get_{field_name}_color', None)
bg_color = get_color() if callable(get_color) else None
return {
'bg_color': bg_color,
}
@@ -254,6 +273,83 @@ class RelatedObjectAttr(ObjectAttribute):
}
class RelatedObjectListAttr(RelatedObjectAttr):
"""
An attribute representing a list of related objects.
The accessor may resolve to a related manager or queryset.
Parameters:
max_items (int): Maximum number of items to display
overflow_indicator (str | None): Marker rendered as a final list item when
additional objects exist beyond `max_items`; set to None to suppress it
"""
template_name = 'ui/attrs/object_list.html'
def __init__(self, *args, max_items=None, overflow_indicator='', **kwargs):
super().__init__(*args, **kwargs)
if max_items is not None and (type(max_items) is not int or max_items < 1):
raise ValueError(
_('Invalid max_items value: {max_items}! Must be a positive integer or None.').format(
max_items=max_items
)
)
self.max_items = max_items
self.overflow_indicator = overflow_indicator
def _get_items(self, obj):
"""
Retrieve items from the given object using the accessor path.
Returns a tuple of (items, has_more) where items is a list of resolved objects
and has_more indicates whether additional items exist beyond the max_items limit.
"""
items = resolve_attr_path(obj, self.accessor)
if items is None:
return [], False
if hasattr(items, 'all'):
items = items.all()
if self.max_items is None:
return list(items), False
items = list(items[:self.max_items + 1])
has_more = len(items) > self.max_items
return items[:self.max_items], has_more
def get_context(self, obj, context):
items, has_more = self._get_items(obj)
return {
'linkify': self.linkify,
'items': [
{
'value': item,
'group': getattr(item, self.grouped_by, None) if self.grouped_by else None,
}
for item in items
],
'overflow_indicator': self.overflow_indicator if has_more else None,
}
def render(self, obj, context):
context = context or {}
context_data = self.get_context(obj, context)
if not context_data['items']:
return self.placeholder
return render_to_string(self.template_name, {
'name': context.get('name'),
**context_data,
})
class NestedObjectAttr(ObjectAttribute):
"""
An attribute representing a related nested object. Similar to `RelatedObjectAttr`, but includes the ancestors of the

View File

@@ -67,6 +67,7 @@ class Panel:
return {
'request': context.get('request'),
'object': context.get('object'),
'perms': context.get('perms'),
'title': self.title,
'actions': self.actions,
'panel_class': self.__class__.__name__,

View File

@@ -1,104 +1,6 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.provider.pk }}">{{ object.provider }}</a></li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Account" %}</th>
<td>{{ object.provider_account|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.cid }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.type|linkify }}</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 "Distance" %}</th>
<td>
{% if object.distance is not None %}
{{ object.distance|floatformat }} {{ object.get_distance_unit_display }}
{% 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 "Install Date" %}</th>
<td>{{ object.install_date|isodate|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Termination Date" %}</th>
<td>{{ object.termination_date|isodate|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Commit Rate" %}</th>
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">
{% trans "Group Assignments" %}
{% if perms.circuits.add_circuitgroupassignment %}
<div class="card-actions">
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'circuits:circuitgroupassignment_list' circuit_id=object.pk %}
</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 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% 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 %}

View File

@@ -0,0 +1,2 @@
{% load helpers %}
{{ value|humanize_speed }}

View File

@@ -1,30 +0,0 @@
{% extends 'generic/confirmation_form.html' %}
{% load i18n %}
{% block title %}{% trans "Swap Circuit Terminations" %}{% endblock %}
{% block message %}
<p>
{% blocktrans trimmed %}
Swap these terminations for circuit {{ circuit }}?
{% endblocktrans %}
</p>
<ul>
<li>
<strong>{% trans "A side" %}:</strong>
{% if termination_a %}
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
{% else %}
{% trans "None" %}
{% endif %}
</li>
<li>
<strong>{% trans "Z side" %}:</strong>
{% if termination_z %}
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
{% else %}
{% trans "None" %}
{% endif %}
</li>
</ul>
{% endblock %}

View File

@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -17,40 +13,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit 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 "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|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/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,9 +1,4 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -11,42 +6,3 @@
<a href="{% url 'circuits:circuitgroupassignment_list' %}?group_id={{ object.group_id }}">{{ object.group }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit Group Assignment" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Group" %}</th>
<td>{{ object.group|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.member.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Circuit" %}</th>
<td>{{ object.member|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Priority" %}</th>
<td>{{ object.get_priority_display }}</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/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -7,45 +7,3 @@
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.circuit.provider.pk }}">{{ object.circuit.provider }}</a></li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
{% if object %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Circuit" %}</th>
<td>
{{ object.circuit|linkify }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>
{{ object.circuit.provider|linkify }}
</td>
</tr>
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
</table>
{% else %}
<div class="card-body">
<span class="text-muted">{% trans "None" %}</span>
</div>
{% endif %}
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,46 +8,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Circuit Type" %}</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>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% 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/comments.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 %}

View File

@@ -55,31 +55,33 @@
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
</ul>
</div>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Speed" %}</th>
<td>
{% if termination.port_speed and termination.upstream_speed %}
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
{% elif termination.port_speed %}
{{ termination.port_speed|humanize_speed }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Speed" %}</th>
<td>
{% if termination.port_speed and termination.upstream_speed %}
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
{% elif termination.port_speed %}
{{ termination.port_speed|humanize_speed }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<th scope="row">{% trans "Cross-Connect" %}</th>
<td>{{ termination.xconnect_id|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Cross-Connect" %}</th>
<td>{{ termination.xconnect_id|placeholder }}</td>
<th scope="row">{% trans "Patch Panel/Port" %}</th>
<td>{{ termination.pp_info|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Patch Panel/Port" %}</th>
<td>{{ termination.pp_info|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ termination.description|placeholder }}</td>
<th scope="row">{% trans "Description" %}</th>
<td>{{ termination.description|placeholder }}</td>
</tr>

View File

@@ -0,0 +1,69 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% blocktrans %}Termination{% endblocktrans %} {{ side }}
<div class="card-actions">
{% if not termination and perms.circuits.add_circuittermination %}
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
</a>
{% endif %}
{% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</a>
{% endif %}
{% if termination and perms.circuits.delete_circuittermination %}
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</a>
{% endif %}
</div>
</h2>
{% if termination %}
<table class="table table-hover attr-table">
{% include 'circuits/inc/circuit_termination_fields.html' with termination=termination %}
<tr>
<th scope="row">{% trans "Tags" %}</th>
<td>
{% for tag in termination.tags.all %}
{% tag tag %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
{% for group_name, fields in termination.get_custom_fields_by_group.items %}
<tr>
<td colspan="2">
{% trans "Custom Fields" as default_group_label %}
<strong>{{ group_name|default:default_group_label }}</strong>
</td>
</tr>
{% for field, value in fields.items %}
<tr>
<th scope="row">{{ 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>
{% else %}
<div class="card-body">
<span class="text-muted">{% trans "None" %}</span>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,16 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Circuit" %}</th>
<td>{{ object.circuit|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.circuit.provider|linkify|placeholder }}</td>
</tr>
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
</table>
{% endblock panel_content %}

View File

@@ -12,52 +12,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "ASNs" %}</th>
<td>
{% for asn in object.asns.all %}
{{ asn|linkify }}{% if not forloop.last %}, {% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</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-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">
<div class="card">
<h2 class="card-header">{% trans "Provider Accounts" %}</h2>
{% htmx_table 'circuits:provideraccount_list' provider_id=object.pk %}
</div>
</div>
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Circuits" %}</h2>
{% htmx_table 'circuits:circuit_list' provider_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,54 +1,6 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider Account" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Account" %}</th>
<td>{{ object.account }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</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/comments.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Circuits" %}</h2>
{% htmx_table 'circuits:circuit_list' provider_account_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,69 +1,6 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'circuits:providernetwork_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Provider Network" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Service ID" %}</th>
<td>{{ object.service_id|placeholder }}</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/comments.html' %}
{% 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 "Circuits" %}</h2>
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
</div>
<div class="card">
<h2 class="card-header">
{% trans "Virtual Circuits" %}
{% if perms.circuits.add_virtualcircuit %}
<div class="card-actions">
<a href="{% url 'circuits:virtualcircuit_add' %}?provider_network={{ 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 Virtual Circuit" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'circuits:virtualcircuit_list' provider_network_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -12,90 +9,3 @@
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.provider_network.pk }}">{{ object.provider_network }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual circuit" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider Network" %}</th>
<td>{{ object.provider_network|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider account" %}</th>
<td>{{ object.provider_account|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.cid }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.type|linkify }}</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 "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/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
<div class="card">
<h2 class="card-header">
{% trans "Group Assignments" %}
{% if perms.circuits.add_circuitgroupassignment %}
<div class="card-actions">
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'circuits:circuitgroupassignment_list' virtual_circuit_id=object.pk %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Terminations" %}
{% if perms.circuits.add_virtualcircuittermination %}
<div class="card-actions">
<a href="{% url 'circuits:virtualcircuittermination_add' %}?virtual_circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Termination" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'circuits:virtualcircuittermination_list' virtual_circuit_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -15,67 +13,3 @@
<a href="{% url 'circuits:virtualcircuittermination_list' %}?virtual_circuit_id={{ object.virtual_circuit.pk }}">{{ object.virtual_circuit }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit Termination" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.virtual_circuit.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider Network" %}</th>
<td>{{ object.virtual_circuit.provider_network|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider account" %}</th>
<td>{{ object.virtual_circuit.provider_account|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Virtual circuit" %}</th>
<td>{{ object.virtual_circuit|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Interface" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.interface.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Interface" %}</th>
<td>{{ object.interface|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.interface.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.interface.description|placeholder }}</td>
</tr>
</table>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,46 +8,3 @@
</a>
{% endif %}
{% endblock extra_controls %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit Type" %}</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>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% 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/comments.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 %}

View File

@@ -1,10 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load static %}
{% load i18n %}
{% block breadcrumbs %}
@@ -27,22 +24,3 @@
</div>
{% endif %}
{% endblock subtitle %}
{% block content %}
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{{ object.comment|placeholder }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,62 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
<h2 class="card-header">{% trans "Data File" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Source" %}</th>
<td>{{ object.source|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Path" %}</th>
<td>
<span class="font-monospace" id="datafile_path">{{ object.path }}</span>
{% copy_content "datafile_path" %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Last Updated" %}</th>
<td>{{ object.last_updated }}</td>
</tr>
<tr>
<th scope="row">{% trans "Size" %}</th>
<td>{{ object.size }} {% trans "bytes" %}</td>
</tr>
<tr>
<th scope="row">{% trans "SHA256 Hash" %}</th>
<td>
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
{% copy_content "datafile_hash" %}
</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Content" %}</h2>
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
</div>
{% plugin_left_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{% load i18n %}{{ value }} {% trans "bytes" %}

View File

@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -23,102 +19,3 @@
{% endif %}
{% endif %}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Data Source" %}</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 "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</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 "Sync interval" %}</th>
<td>{{ object.get_sync_interval_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last synced" %}</th>
<td>{{ object.last_synced|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "URL" %}</th>
<td>
{% if not object.type.is_local %}
<a href="{{ object.source_url }}">{{ object.source_url }}</a>
{% else %}
{{ object.source_url }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Ignore rules" %}</th>
<td>
{% if object.ignore_rules %}
<pre>{{ object.ignore_rules }}</pre>
{% else %}
{{ ''|placeholder }}
{% endif %}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Backend" %}</h2>
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
</div>
{% 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">
<div class="card">
<h2 class="card-header">{% trans "Files" %}</h2>
{% htmx_table 'core:datafile_list' source_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<pre>{{ value }}</pre>

View File

@@ -0,0 +1 @@
{% if not object.type.is_local %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value }}{% endif %}

View File

@@ -1,78 +1 @@
{% extends 'core/job/base.html' %}
{% load i18n %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Job" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display object.get_status_color %}</td>
</tr>
{% if object.error %}
<tr>
<th scope="row">{% trans "Error" %}</th>
<td>{{ object.error }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Created By" %}</th>
<td>{{ object.user|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Scheduling" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "Scheduled" %}</th>
<td>
{{ object.scheduled|isodatetime|placeholder }}
{% if object.interval %}
({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Started" %}</th>
<td>{{ object.started|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Completed" %}</th>
<td>{{ object.completed|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Queue" %}</th>
<td>{{ object.queue_name|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col col-12">
<div class="card">
<h2 class="card-header">{% trans "Data" %}</h2>
<pre class="card-body m-0">{{ object.data|json }}</pre>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ value }}</a>

View File

@@ -0,0 +1,3 @@
{% load helpers %}
{% load i18n %}
{{ value|isodatetime }}{% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}){% endif %}

View File

@@ -1,12 +1 @@
{% extends 'core/job/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object }}{% endblock %}
@@ -21,161 +19,3 @@
{# ObjectChange does not support the default add/edit/delete controls #}
{% block control-buttons %}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Change" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Time" %}</th>
<td>{{ object.time|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "User" %}</th>
<td>
{% if object.user.get_full_name %}
{{ object.user.get_full_name }} ({{ object.user_name }})
{% else %}
{{ object.user_name }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Action" %}</th>
<td>
{{ object.get_action_display }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
{{ object.changed_object_type }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object" %}</th>
<td>
{% if object.changed_object and object.changed_object.get_absolute_url %}
{{ object.changed_object|linkify }}
{% else %}
{{ object.object_repr }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Message" %}</th>
<td>
{{ object.message|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Request ID" %}</th>
<td>
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,2 @@
{% load helpers %}
{% if object.changed_object and object.changed_object.get_absolute_url %}{{ object.changed_object|linkify }}{% else %}{{ value }}{% endif %}

View File

@@ -0,0 +1 @@
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>

View File

@@ -0,0 +1 @@
{% if object.user and object.user.get_full_name %}{{ object.user.get_full_name }} ({{ value }}){% else %}{{ value }}{% endif %}

View File

@@ -0,0 +1,11 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{% if object.comment %}
{{ object.comment }}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>

View File

@@ -0,0 +1,8 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{% block panel_content %}
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
{% endblock panel_content %}

View File

@@ -0,0 +1,26 @@
{% extends "ui/panels/_base.html" %}
{% load helpers %}
{% load i18n %}
{% block panel_content %}
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
{% endblock panel_content %}

View File

@@ -0,0 +1,31 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,18 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,20 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,11 @@
{% load i18n %}
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}

View File

@@ -1,87 +1 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load perms %}
{% 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 "Cable" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display|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 "Profile" %}</th>
<td>{% badge object.get_profile_display %}</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 "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Length" %}</th>
<td>
{% if object.length is not None %}
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
{% 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">
<div class="card">
<h2 class="card-header">{% trans "Termination" %} A</h2>
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Termination" %} B</h2>
{% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,88 +7,3 @@
<a href="{% url 'dcim:device_consoleports' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Console Port" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Speed" %}</th>
<td>{{ object.get_speed_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %}
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Console Server Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Front Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Rear Port" %}</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,88 +7,3 @@
<a href="{% url 'dcim:device_consoleserverports' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Console Server Port" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Speed" %}</th>
<td>{{ object.get_speed_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %}
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Console Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Front Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Rear Port" %}</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,63 +7,3 @@
<a href="{% url 'dcim:device_devicebays' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Device Bay" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Installed Device" %}</h2>
{% if object.installed_device %}
{% with device=object.installed_device %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Device Type" %}</th>
<td>{{ device.device_type }}</td>
</tr>
</table>
{% endwith %}
{% else %}
<div class="card-body text-muted">
{% trans "None" %}
</div>
{% endif %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,149 +7,3 @@
<a href="{% url 'dcim:device_frontports' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Front Port" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Positions" %}</th>
<td>{{ object.positions }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> {% trans "Marked as Connected" %}
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:frontport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Connection Status" %}</th>
<td>
{% if object.cable.status %}
<span class="badge text-bg-success">{{ object.cable.get_status_display }}</span>
{% else %}
<span class="badge text-bg-info">{{ object.cable.get_status_display }}</span>
{% endif %}
</td>
</tr>
</table>
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}">{% trans "Console Server Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}">{% trans "Console Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
<table class="table table-hover">
{% if rear_port_mappings %}
<thead>
<tr>
<th>{% trans "Position" %}</th>
<th>{% trans "Rear Port" %}</th>
</tr>
</thead>
{% endif %}
{% for mapping in rear_port_mappings %}
<tr>
<td>{{ mapping.front_port_position }}</td>
<td>
<a href="{{ mapping.rear_port.get_absolute_url }}">{{ mapping.rear_port }}:{{ mapping.rear_port_position }}</a>
</td>
</tr>
{% empty %}
{% trans "No mappings defined" %}
{% endfor %}
</table>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -19,436 +16,3 @@
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Interface" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Speed/Duplex" %}</th>
<td>
{{ object.speed|humanize_speed|placeholder }} /
{{ object.get_duplex_display|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "MTU" %}</th>
<td>{{ object.mtu|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Management Only" %}</th>
<td>{% checkmark object.mgmt_only %}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }} </td>
</tr>
<tr>
<th scope="row">{% trans "PoE Mode" %}</th>
<td>{{ object.get_poe_mode_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "PoE Type" %}</th>
<td>{{ object.get_poe_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "802.1Q Mode" %}</th>
<td>{{ object.get_mode_display|placeholder }}</td>
</tr>
{% if object.mode == 'q-in-q' %}
<tr>
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
</tr>
{% elif object.mode %}
<tr>
<th scope="row">{% trans "Untagged VLAN" %}</th>
<td>{{ object.untagged_vlan|linkify|placeholder }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Transmit power (dBm)" %}</th>
<td>{{ object.tx_power|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tunnel" %}</th>
<td>{{ object.tunnel_termination.tunnel|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "L2VPN" %}</th>
<td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Related Interfaces" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Parent" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Bridge" %}</th>
<td>{{ object.bridge|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Bridged Interfaces" %}</th>
<td>
{% if bridge_interfaces %}
{% for interface in bridge_interfaces %}
{{ interface|linkify }}
{% if not forloop.last %}<br />{% endif %}
{% endfor %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "LAG" %}</th>
<td>{{ object.lag|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=vdc_table heading="Virtual Device Contexts" %}
<div class="card">
<h2 class="card-header">{% trans "Addressing" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "MAC Address" %}</th>
<td>
{% if object.primary_mac_address %}
<span class="font-monospace">{{ object.primary_mac_address|linkify }}</span>
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "WWN" %}</th>
<td>
{% if object.wwn %}
<span class="font-monospace">{{ object.wwn }}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>{{ object.vrf|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN Translation" %}</th>
<td>{{ object.vlan_translation_policy|linkify|placeholder }}</td>
</tr>
</table>
</div>
{% if object.is_virtual and object.virtual_circuit_termination %}
<div class="card">
<h2 class="card-header">{% trans "Virtual Circuit" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider Network" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider_network|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{{ object.virtual_circuit_termination.get_role_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Connections" %}</th>
<td>
{% for termination in object.virtual_circuit_termination.peer_terminations %}
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
<i class="mdi mdi-chevron-right"></i>
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
({{ termination.get_role_display }})
{% if not forloop.last %}<br />{% endif %}
{% endfor %}
</td>
</tr>
</table>
</div>
{% elif not object.is_virtual %}
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as Connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:interface_trace' %}
{% elif object.wireless_link %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Wireless Link" %}</th>
<td>
{{ object.wireless_link|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% with peer_interface=object.link_peers.0 %}
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ peer_interface.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ peer_interface|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ peer_interface.get_type_display }}</td>
</tr>
{% endwith %}
</table>
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if object.is_wired and perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a>
</li>
</ul>
</div>
{% elif object.is_wireless and perms.wireless.add_wirelesslink %}
<div class="dropdown float-end">
<a href="{% url 'wireless:wirelesslink_add' %}?interface_a={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
<span class="mdi mdi-wifi-plus" aria-hidden="true"></span> {% trans "Connect" %}
</a>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if object.is_wireless %}
<div class="card">
<h2 class="card-header">{% trans "Wireless" %}</h2>
{% with peer=object.connected_endpoints.0 %}
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th>{% trans "Local" %}</th>
{% if peer %}
<th>{% trans "Peer" %}</th>
{% endif %}
</tr>
</thead>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{{ object.get_rf_role_display|placeholder }}</td>
{% if peer %}
<td>{{ peer.get_rf_role_display|placeholder }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel" %}</th>
<td>{{ object.get_rf_channel_display|placeholder }}</td>
{% if peer %}
<td{% if peer.rf_channel != object.rf_channel %} class="text-danger"{% endif %}>
{{ peer.get_rf_channel_display|placeholder }}
</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel Frequency" %}</th>
<td>
{% if object.rf_channel_frequency %}
{{ object.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
<td{% if peer.rf_channel_frequency != object.rf_channel_frequency %} class="text-danger"{% endif %}>
{% if peer.rf_channel_frequency %}
{{ peer.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel Width" %}</th>
<td>
{% if object.rf_channel_width %}
{{ object.rf_channel_width|floatformat:"-3" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
<td{% if peer.rf_channel_width != object.rf_channel_width %} class="text-danger"{% endif %}>
{% if peer.rf_channel_width %}
{{ peer.rf_channel_width|floatformat:"-3" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}
</tr>
</table>
{% endwith %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Wireless LANs" %}</h2>
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Group" %}</th>
<th>{% trans "SSID" %}</th>
</tr>
</thead>
<tbody>
{% for wlan in object.wireless_lans.all %}
<tr>
<td>{{ wlan.group|linkify|placeholder }}</td>
<td>{{ wlan|linkify:"ssid" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">{% trans "None" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include 'ipam/inc/panels/fhrp_groups.html' %}
{% include 'dcim/inc/panels/inventory_items.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 "IP Addresses" %}
{% if perms.ipam.add_ipaddress %}
<div class="card-actions">
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add IP Address" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:ipaddress_list' interface_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "MAC Addresses" %}
{% if perms.dcim.add_macaddress %}
<div class="card-actions">
<a href="{% url 'dcim:macaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add MAC Address" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'dcim:macaddress_list' interface_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "VLANs" %}</h2>
{% htmx_table 'ipam:vlan_list' interface_id=object.pk %}
</div>
</div>
</div>
{% if object.is_lag %}
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=lag_interfaces_table heading="LAG Members" %}
</div>
</div>
{% endif %}
{% if object.vlan_translation_policy %}
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=vlan_translation_table heading="VLAN Translation" %}
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=bridge_interfaces_table heading="Bridged Interfaces" %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
{% load helpers i18n %}
<span class="font-monospace">{{ value|linkify }}</span>
<span class="badge text-bg-primary">{% trans "Primary" %}</span>

View File

@@ -0,0 +1,2 @@
{% load helpers %}
{{ value|humanize_speed }}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,74 +7,3 @@
<a href="{% url 'dcim:device_inventory' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Inventory Item" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Parent Item" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|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 "Role" %}</th>
<td>{{ object.role|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Component" %}</th>
<td>{{ object.component|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Part ID" %}</th>
<td>{{ object.part_id|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Serial" %}</th>
<td>{{ object.serial|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Asset Tag" %}</th>
<td>{{ object.asset_tag|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% 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 %}

View File

@@ -1,53 +1 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'dcim:inventoryitemrole_list' %}">{% trans "Inventory Item Roles" %}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Inventory Item 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 }}">&nbsp;</span>
</td>
</tr>
<tr>
<th scope="row">{% trans "Inventory Items" %}</th>
<td>
<a href="{% url 'dcim:inventoryitem_list' %}?role_id={{ object.pk }}">{{ inventoryitem_count }}</a>
</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/comments.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 %}

View File

@@ -1,55 +1 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "MAC Address" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "MAC Address" %}</th>
<td>
<span id="macaddress_{{ object.pk }}">{{ object.mac_address|placeholder }}</span>
{% copy_content object.pk prefix="macaddress_" %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Assignment" %}</th>
<td>
{% if object.assigned_object %}
{{ object.assigned_object.parent_object|linkify }} /
{{ object.assigned_object|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Primary for interface" %}</th>
<td>{% checkmark object.is_primary %}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,83 +7,3 @@
<a href="{% url 'dcim:device_modulebays' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Module Bay" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>
<a href="{% url 'dcim:device_modulebays' pk=object.device.pk %}">{{ object.device }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Position" %}</th>
<td>{{ object.position|placeholder }}</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/custom_fields.html' %}
<div class="card">
<h2 class="card-header">{% trans "Installed Module" %}</h2>
{% if object.installed_module %}
{% with module=object.installed_module %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ module|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ module.module_type.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module Type" %}</th>
<td>{{ module.module_type|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Serial Number" %}</th>
<td class="font-monospace">{{ module.serial|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Asset Tag" %}</th>
<td class="font-monospace">{{ module.asset_tag|placeholder }}</td>
</tr>
</table>
{% endwith %}
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}
{% endblock panel_content %}

View File

@@ -0,0 +1,6 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %}
{% endblock panel_content %}

View File

@@ -0,0 +1,40 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Label" %}</th>
<th>{% trans "Role" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for item in object.inventory_items.all %}
<tr>
<td>{{ item|linkify:"name" }}</td>
<td>{{ item.label|placeholder }}</td>
<td>{{ item.role|linkify|placeholder }}</td>
<td class="text-end d-print-none">
{% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-muted">{% trans "None" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,96 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as connected" %}
</div>
{% elif object.cable %}
{% if show_endpoints %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url trace_url_name pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Path status" %}</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge text-bg-success">{% trans "Reachable" %}</span>
{% else %}
<span class="badge text-bg-danger">{% trans "Not Reachable" %}</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Path endpoints" %}</th>
<td>
{% for endpoint in object.connected_endpoints %}
{% if endpoint.parent_object %}
{{ endpoint.parent_object|linkify }}
<i class="mdi mdi-chevron-right"></i>
{% endif %}
{{ endpoint|linkify }}
{% if not forloop.last %}<br />{% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
</table>
{% else %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url trace_url_name pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Connection status" %}</th>
<td>
{% if object.cable.status %}
<span class="badge text-bg-success">{{ object.cable.get_status_display }}</span>
{% else %}
<span class="badge text-bg-info">{{ object.cable.get_status_display }}</span>
{% endif %}
</td>
</tr>
</table>
{% endif %}
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
{% if connect_options|length > 1 %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for option in connect_options %}
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type={{ option.a_type }}&a_terminations={{ object.pk }}&b_terminations_type={{ option.b_type }}&return_url={{ object.get_absolute_url }}">{{ option.label }}</a>
</li>
{% endfor %}
</ul>
</div>
{% elif connect_options|length == 1 %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type={{ connect_options.0.a_type }}&a_terminations={{ object.pk }}&b_terminations_type={{ connect_options.0.b_type }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}
</a>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endblock panel_content %}

View File

@@ -0,0 +1,29 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
<table class="table table-hover">
{% if rear_port_mappings %}
<thead>
<tr>
<th>{% trans "Position" %}</th>
<th>{% trans "Rear Port" %}</th>
</tr>
</thead>
{% endif %}
<tbody>
{% for mapping in rear_port_mappings %}
<tr>
<td>{{ mapping.front_port_position }}</td>
<td>
<a href="{{ mapping.rear_port.get_absolute_url }}">{{ mapping.rear_port }}:{{ mapping.rear_port_position }}</a>
</td>
</tr>
{% empty %}
<tr>
<td class="text-muted">{% trans "No mappings defined" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,21 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% if object.installed_device %}
{% with device=object.installed_device %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Device type" %}</th>
<td>{{ device.device_type }}</td>
</tr>
</table>
{% endwith %}
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
{% endblock panel_content %}

View File

@@ -0,0 +1,33 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% if object.installed_module %}
{% with module=object.installed_module %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ module|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ module.module_type.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module type" %}</th>
<td>{{ module.module_type|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Serial number" %}</th>
<td class="font-monospace">{{ module.serial|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Asset tag" %}</th>
<td class="font-monospace">{{ module.asset_tag|placeholder }}</td>
</tr>
</table>
{% endwith %}
{% else %}
<div class="card-body text-muted">{% trans "None" %}</div>
{% endif %}
{% endblock panel_content %}

View File

@@ -0,0 +1,105 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as connected" %}
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Path status" %}</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge text-bg-success">{% trans "Reachable" %}</span>
{% else %}
<span class="badge text-bg-danger">{% trans "Not Reachable" %}</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Path endpoints" %}</th>
<td>
{% for endpoint in object.connected_endpoints %}
{% if endpoint.parent_object %}
{{ endpoint.parent_object|linkify }}
<i class="mdi mdi-chevron-right"></i>
{% endif %}
{{ endpoint|linkify }}
{% if not forloop.last %}<br />{% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
</table>
{% elif object.wireless_link %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Wireless Link" %}</th>
<td>
{{ object.wireless_link|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% with peer_interface=object.link_peers.0 %}
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ peer_interface.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ peer_interface|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ peer_interface.get_type_display }}</td>
</tr>
{% endwith %}
</table>
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if object.is_wired and perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a>
</li>
</ul>
</div>
{% elif object.is_wireless and perms.wireless.add_wirelesslink %}
<div class="dropdown float-end">
<a href="{% url 'wireless:wirelesslink_add' %}?interface_a={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary">
<span class="mdi mdi-wifi-plus" aria-hidden="true"></span> {% trans "Connect" %}
</a>
</div>
{% endif %}
</div>
{% endif %}
{% endblock panel_content %}

View File

@@ -0,0 +1,35 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Provider" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Provider Network" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit.provider_network|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.virtual_circuit_termination.virtual_circuit|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{{ object.virtual_circuit_termination.get_role_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Connections" %}</th>
<td>
{% for termination in object.virtual_circuit_termination.peer_terminations %}
<a href="{{ termination.interface.parent_object.get_absolute_url }}">{{ termination.interface.parent_object }}</a>
<i class="mdi mdi-chevron-right"></i>
<a href="{{ termination.interface.get_absolute_url }}">{{ termination.interface }}</a>
({{ termination.get_role_display }})
{% if not forloop.last %}<br />{% endif %}
{% endfor %}
</td>
</tr>
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,72 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
{% with peer=object.connected_endpoints.0 %}
<table class="table table-hover attr-table">
<thead>
<tr class="border-bottom">
<th></th>
<th>{% trans "Local" %}</th>
{% if peer %}
<th>{% trans "Peer" %}</th>
{% endif %}
</tr>
</thead>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{{ object.get_rf_role_display|placeholder }}</td>
{% if peer %}
<td>{{ peer.get_rf_role_display|placeholder }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel" %}</th>
<td>{{ object.get_rf_channel_display|placeholder }}</td>
{% if peer %}
<td{% if peer.rf_channel != object.rf_channel %} class="text-danger"{% endif %}>
{{ peer.get_rf_channel_display|placeholder }}
</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel frequency" %}</th>
<td>
{% if object.rf_channel_frequency %}
{{ object.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
<td{% if peer.rf_channel_frequency != object.rf_channel_frequency %} class="text-danger"{% endif %}>
{% if peer.rf_channel_frequency %}
{{ peer.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Channel width" %}</th>
<td>
{% if object.rf_channel_width %}
{{ object.rf_channel_width|floatformat:"-3" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
<td{% if peer.rf_channel_width != object.rf_channel_width %} class="text-danger"{% endif %}>
{% if peer.rf_channel_width %}
{{ peer.rf_channel_width|floatformat:"-3" }} {% trans "MHz" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}
</tr>
</table>
{% endwith %}
{% endblock panel_content %}

View File

@@ -0,0 +1,25 @@
{% extends "ui/panels/_base.html" %}
{% load helpers i18n %}
{% block panel_content %}
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Group" %}</th>
<th>{% trans "SSID" %}</th>
</tr>
</thead>
<tbody>
{% for wlan in object.wireless_lans.all %}
<tr>
<td>{{ wlan.group|linkify|placeholder }}</td>
<td>{{ wlan|linkify:"ssid" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">{% trans "None" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,29 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
<table class="table table-hover">
{% if front_port_mappings %}
<thead>
<tr>
<th>{% trans "Position" %}</th>
<th>{% trans "Front Port" %}</th>
</tr>
</thead>
{% endif %}
<tbody>
{% for mapping in front_port_mappings %}
<tr>
<td>{{ mapping.rear_port_position }}</td>
<td>
<a href="{{ mapping.front_port.get_absolute_url }}">{{ mapping.front_port }}:{{ mapping.front_port_position }}</a>
</td>
</tr>
{% empty %}
<tr>
<td class="text-muted">{% trans "No mappings defined" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -17,7 +17,7 @@
<td>{{ vc_member|linkify }}</td>
<td>{% badge vc_member.vc_position show_empty=True %}</td>
<td>
{% if object.virtual_chassis.master == vc_member %}
{% if virtual_chassis.master == vc_member %}
{% checkmark True %}
{% else %}
{{ ''|placeholder }}

View File

@@ -1,9 +1,4 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -13,126 +8,3 @@
<li class="breadcrumb-item"><a href="{% url 'dcim:powerfeed_list' %}?rack_id={{ object.rack.pk }}">{{ object.rack }}</a></li>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Feed" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Power Panel" %}</th>
<td>{{ object.power_panel|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Rack" %}</th>
<td>{{ object.rack|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{% badge object.get_type_display bg_color=object.get_type_color %}</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 "Description" %}</th>
<td>{{ object.description|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 "Connected Device" %}</th>
<td>
{% if object.connected_endpoints %}
{{ object.connected_endpoints.0.device|linkify }} ({{ object.connected_endpoints.0|linkify:"name" }})
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Utilization (Allocated" %})</th>
{% with utilization=object.connected_endpoints.0.get_power_draw %}
{% if utilization %}
<td>
{{ utilization.allocated }}{% trans "VA" %} / {{ object.available_power }}{% trans "VA" %}
{% if object.available_power > 0 %}
{% utilization_graph utilization.allocated|percentage:object.available_power %}
{% endif %}
</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
{% endwith %}
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Electrical Characteristics" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Supply" %}</th>
<td>{{ object.get_supply_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Voltage" %}</th>
<td>{{ object.voltage }}{% trans "V" context "Abbreviation for volts" %}</td>
</tr>
<tr>
<th scope="row">{% trans "Amperage" %}</th>
<td>{{ object.amperage }}{% trans "A" context "Abbreviation for amperes" %}</td>
</tr>
<tr>
<th scope="row">{% trans "Phase" %}</th>
<td>{{ object.get_phase_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Max Utilization" %}</th>
<td>{{ object.max_utilization }}%</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerfeed_trace' %}
{% else %}
<div class="card-body text-muted">
{% trans "Not connected" %}
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% load helpers %}
{% if value %}
{{ value.0.device|linkify }} ({{ value.0|linkify:"name" }})
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -0,0 +1,15 @@
{% load helpers i18n %}
{% if value %}
{% with utilization=value.0.get_power_draw %}
{% if utilization %}
{{ utilization.allocated }}{% trans "VA" %} / {{ object.available_power }}{% trans "VA" %}
{% if object.available_power > 0 %}
{% utilization_graph utilization.allocated|percentage:object.available_power %}
{% endif %}
{% else %}
{{ ''|placeholder }}
{% endif %}
{% endwith %}
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,93 +7,3 @@
<a href="{% url 'dcim:device_poweroutlets' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Outlet" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</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 "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Power Port" %}</th>
<td>{{ object.power_port|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Feed Leg" %}</th>
<td>{{ object.get_feed_leg_display|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as Connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:poweroutlet_trace' %}
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" title="{% trans "Connect" %}" class="btn btn-primary float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.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 %}

View File

@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -11,72 +7,3 @@
<li class="breadcrumb-item">{{ object.location|linkify }}</li>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Panel" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>{{ object.site|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Location" %}</th>
<td>{{ object.location|linkify|placeholder }}</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-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row my-3">
<div class="col col-md-12">
<form method="post">
{% csrf_token %}
<div class="card">
<h2 class="card-header">{% trans "Power Feeds" %}</h2>
{% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %}
<div class="card-footer d-print-none">
{% if perms.dcim.change_powerfeed %}
<button type="submit" name="_edit" {% formaction %}="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</button>
{% endif %}
{% if perms.dcim.delete_cable %}
<button type="submit" name="_disconnect" {% formaction %}="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button>
{% endif %}
{% if perms.dcim.delete_powerfeed %}
<button type="submit" name="_delete" {% formaction %}="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</button>
{% endif %}
{% if perms.dcim.add_powerfeed %}
<div class="float-end">
<a href="{% url 'dcim:powerfeed_add' %}?power_panel={{ object.pk }}&return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Power Feeds" %}
</a>
</div>
{% endif %}
</div>
</div>
</form>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,89 +7,3 @@
<a href="{% url 'dcim:device_powerports' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Power Port" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Maximum Draw" %}</th>
<td>{{ object.maximum_draw|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Allocated Draw" %}</th>
<td>{{ object.allocated_draw|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
{% trans "Marked as Connected" %}
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:powerport_trace' %}
{% else %}
<div class="card-body text-muted">
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<span class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ object.pk }}&b_terminations_type=dcim.poweroutlet&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Power Outlet" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerfeed&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Power Feed" %}</a>
</li>
</ul>
</span>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,4 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -9,143 +7,3 @@
<a href="{% url 'dcim:device_rearports' pk=object.device.pk %}">{{ object.device }}</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Rear Port" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Positions" %}</th>
<td>{{ object.positions }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2>
{% if object.mark_connected %}
<div class="card-body text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> {% trans "Marked as Connected" %}
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:rearport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Connection Status" %}</th>
<td>
{% if object.cable.status %}
<span class="badge text-bg-success">{{ object.cable.get_status_display }}</span>
{% else %}
<span class="badge text-bg-info">{{ object.cable.get_status_display }}</span>
{% endif %}
</td>
</tr>
</table>
{% else %}
<div class="card-body text-muted">
{% trans "Not connected" %}
{% if perms.dcim.add_cable %}
<span class="dropdown float-end">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Interface" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Front Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Rear Port" %}</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Circuit Termination" %}</a>
</li>
</ul>
</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Port Mappings" %}</h2>
<table class="table table-hover">
{% if front_port_mappings %}
<thead>
<tr>
<th>{% trans "Position" %}</th>
<th>{% trans "Front Port" %}</th>
</tr>
</thead>
{% endif %}
{% for mapping in front_port_mappings %}
<tr>
<td>{{ mapping.rear_port_position }}</td>
<td>
<a href="{{ mapping.front_port.get_absolute_url }}">{{ mapping.front_port }}:{{ mapping.front_port_position }}</a>
</td>
</tr>
{% empty %}
{% trans "No mappings defined" %}
{% endfor %}
</table>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More