mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-01 15:13:27 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05059f4a86 | ||
|
|
e4e4c1c56d | ||
|
|
c99d8481b2 | ||
|
|
0923a3dec8 | ||
|
|
80b9c25674 | ||
|
|
6d13bc8b96 | ||
|
|
ee17e83da6 | ||
|
|
5ab9608e38 | ||
|
|
e54ed87863 | ||
|
|
55daf4c52f | ||
|
|
a45e8571da | ||
|
|
0154a09856 | ||
|
|
757c4f69d2 | ||
|
|
d5f37d7a87 | ||
|
|
f30786d8fe | ||
|
|
bb73601d80 | ||
|
|
99e9d96787 | ||
|
|
f5c97e367c | ||
|
|
ea756b29e9 | ||
|
|
b929e1aa1b | ||
|
|
91d5382a61 | ||
|
|
e76203238d | ||
|
|
3f58648115 | ||
|
|
b904dc5c75 | ||
|
|
bf27ff9593 | ||
|
|
981f31304d | ||
|
|
2a39ab47d6 | ||
|
|
aa01c16db0 | ||
|
|
e04986617c | ||
|
|
83cf193cdc | ||
|
|
d497198f49 | ||
|
|
4e479c547f | ||
|
|
e44c0a2119 | ||
|
|
3ab0613708 | ||
|
|
9f16734266 | ||
|
|
c3c7cf15b2 | ||
|
|
268ef4f59f | ||
|
|
671b1cd470 |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.5
|
||||
placeholder: v4.5.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.5
|
||||
placeholder: v4.5.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.5
|
||||
placeholder: v4.5.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/claude-code-review.yml
vendored
2
.github/workflows/claude-code-review.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/close-stale-issues.yml
vendored
2
.github/workflows/close-stale-issues.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||
|
||||
2
.github/workflows/lock-threads.yml
vendored
2
.github/workflows/lock-threads.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ python manage.py nbshell # NetBox-enhanced shell
|
||||
|
||||
## Architecture Conventions
|
||||
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
|
||||
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`.
|
||||
- **Views**: Use `register_model_view()` to register model views by action (e.g. "add", "list", etc.). List views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the table class so that only relevant fields are prefetched.
|
||||
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`. REST API views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the serializer so that only relevant fields are prefetched.
|
||||
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
|
||||
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
|
||||
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
|
||||
@@ -68,6 +69,8 @@ python manage.py nbshell # NetBox-enhanced shell
|
||||
- API serializers must include a `url` field (absolute URL of the object).
|
||||
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
|
||||
- Avoid adding new dependencies without strong justification.
|
||||
- Avoid running `ruff format` on existing files, as this tends to introduce unnecessary style changes.
|
||||
- Don't craft Django database migrations manually: Prompt the user to run `manage.py makemigrations` instead.
|
||||
|
||||
## Branch & PR Conventions
|
||||
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
|
||||
|
||||
@@ -416,9 +416,13 @@
|
||||
"800gbase-dr8",
|
||||
"800gbase-sr8",
|
||||
"800gbase-vr8",
|
||||
"1.6tbase-cr8",
|
||||
"1.6tbase-dr8",
|
||||
"1.6tbase-dr8-2",
|
||||
"100base-x-sfp",
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"2.5gbase-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
"10gbase-x-xenpak",
|
||||
"10gbase-x-xfp",
|
||||
@@ -448,6 +452,9 @@
|
||||
"400gbase-x-osfp-rhs",
|
||||
"800gbase-x-osfp",
|
||||
"800gbase-x-qsfpdd",
|
||||
"1.6tbase-x-osfp1600",
|
||||
"1.6tbase-x-osfp1600-rhs",
|
||||
"1.6tbase-x-qsfpdd1600",
|
||||
"1000base-kx",
|
||||
"2.5gbase-kx",
|
||||
"5gbase-kr",
|
||||
@@ -459,6 +466,7 @@
|
||||
"100gbase-kp4",
|
||||
"100gbase-kr2",
|
||||
"100gbase-kr4",
|
||||
"1.6tbase-kr8",
|
||||
"ieee802.11a",
|
||||
"ieee802.11g",
|
||||
"ieee802.11n",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,24 @@
|
||||
# NetBox v4.5
|
||||
|
||||
## v4.5.6 (2026-03-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#21480](https://github.com/netbox-community/netbox/issues/21480) - Add OSFP224 (1.6T) interface type
|
||||
* [#21727](https://github.com/netbox-community/netbox/issues/21727) - Add 2.5GBASE-X SFP modular interface type
|
||||
* [#21743](https://github.com/netbox-community/netbox/issues/21743) - Improve object change diff styling and layout
|
||||
* [#21793](https://github.com/netbox-community/netbox/issues/21793) - Add 50 Gbps, 800 Gbps, and 1.6 Tbps interface speed options
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20467](https://github.com/netbox-community/netbox/issues/20467) - Fix resolution of the `{module}` variable for position fields in nested modules
|
||||
* [#21698](https://github.com/netbox-community/netbox/issues/21698) - Adjust custom field URL filter to support non-standard port numbers
|
||||
* [#21707](https://github.com/netbox-community/netbox/issues/21707) - Fix grouping of owner fields in provider account add/edit forms
|
||||
* [#21749](https://github.com/netbox-community/netbox/issues/21749) - Fix `FieldError` exception when sorting the circuit group assignment table by the member column
|
||||
* [#21763](https://github.com/netbox-community/netbox/issues/21763) - Use separate add/remove form fields when editing a site or provider with a large number of ASNs assigned
|
||||
|
||||
---
|
||||
|
||||
## v4.5.5 (2026-03-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -22,7 +22,7 @@ from utilities.forms.fields import (
|
||||
SlugField,
|
||||
)
|
||||
from utilities.forms.mixins import DistanceValidationMixin
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
@@ -48,17 +48,42 @@ class ProviderForm(PrimaryModelForm):
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
add_asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('Add ASNs'),
|
||||
required=False
|
||||
)
|
||||
remove_asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('Remove ASNs'),
|
||||
required=False
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'asns', 'description', 'tags'),
|
||||
FieldSet('name', 'slug', M2MAddRemoveFields('asns'), 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags',
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD:
|
||||
# Add/remove mode for large M2M sets
|
||||
self.fields.pop('asns')
|
||||
self.fields['add_asns'].widget.add_query_param('provider_id__n', self.instance.pk)
|
||||
self.fields['remove_asns'].widget.add_query_param('provider_id', self.instance.pk)
|
||||
self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count)
|
||||
else:
|
||||
# Simple mode for new objects or small M2M sets
|
||||
self.fields.pop('add_asns')
|
||||
self.fields.pop('remove_asns')
|
||||
if self.instance.pk:
|
||||
self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True))
|
||||
|
||||
|
||||
class ProviderAccountForm(PrimaryModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
@@ -68,10 +93,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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -190,14 +190,16 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
provider = tables.Column(
|
||||
accessor='member__provider',
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
)
|
||||
member_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
member = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
)
|
||||
priority = tables.Column(
|
||||
verbose_name=_('Priority'),
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
from django.test import RequestFactory, TestCase, tag
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.tables import CircuitTerminationTable
|
||||
from circuits.models import CircuitGroupAssignment, CircuitTermination
|
||||
from circuits.tables import CircuitGroupAssignmentTable, CircuitTerminationTable
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class CircuitTerminationTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
terminations = CircuitTermination.objects.all()
|
||||
disallowed = {'actions', }
|
||||
disallowed = {
|
||||
'actions',
|
||||
}
|
||||
|
||||
orderable_columns = [
|
||||
column.name for column in CircuitTerminationTable(terminations).columns
|
||||
column.name
|
||||
for column in CircuitTerminationTable(terminations).columns
|
||||
if column.orderable and column.name not in disallowed
|
||||
]
|
||||
fake_request = RequestFactory().get("/")
|
||||
fake_request = RequestFactory().get('/')
|
||||
|
||||
for col in orderable_columns:
|
||||
for dir in ('-', ''):
|
||||
for direction in ('-', ''):
|
||||
table = CircuitTerminationTable(terminations)
|
||||
table.order_by = f'{dir}{col}'
|
||||
table.order_by = f'{direction}{col}'
|
||||
table.as_html(fake_request)
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class CircuitGroupAssignmentTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
assignment = CircuitGroupAssignment.objects.all()
|
||||
disallowed = {
|
||||
'actions',
|
||||
}
|
||||
|
||||
orderable_columns = [
|
||||
column.name
|
||||
for column in CircuitGroupAssignmentTable(assignment).columns
|
||||
if column.orderable and column.name not in disallowed
|
||||
]
|
||||
fake_request = RequestFactory().get('/')
|
||||
|
||||
for col in orderable_columns:
|
||||
for direction in ('-', ''):
|
||||
table = CircuitGroupAssignmentTable(assignment)
|
||||
table.order_by = f'{direction}{col}'
|
||||
table.as_html(fake_request)
|
||||
|
||||
0
netbox/circuits/ui/__init__.py
Normal file
0
netbox/circuits/ui/__init__.py
Normal file
139
netbox/circuits/ui/panels.py
Normal file
139
netbox/circuits/ui/panels.py
Normal 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')
|
||||
@@ -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')
|
||||
|
||||
0
netbox/core/ui/__init__.py
Normal file
0
netbox/core/ui/__init__.py
Normal file
91
netbox/core/ui/panels.py
Normal file
91
netbox/core/ui/panels.py
Normal 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',
|
||||
)
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -1003,10 +1003,16 @@ 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'
|
||||
TYPE_1GE_SFP = '1000base-x-sfp'
|
||||
TYPE_2GE_SFP = '2.5gbase-x-sfp'
|
||||
TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp'
|
||||
TYPE_10GE_XFP = '10gbase-x-xfp'
|
||||
TYPE_10GE_XENPAK = '10gbase-x-xenpak'
|
||||
@@ -1034,8 +1040,11 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_400GE_OSFP_RHS = '400gbase-x-osfp-rhs'
|
||||
TYPE_400GE_CDFP = '400gbase-x-cdfp'
|
||||
TYPE_400GE_CFP8 = '400gbase-x-cfp8'
|
||||
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
|
||||
TYPE_800GE_OSFP = '800gbase-x-osfp'
|
||||
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd' # TODO: Rename to _QSFP_DD800
|
||||
TYPE_800GE_OSFP = '800gbase-x-osfp' # TODO: Rename to _OSFP800
|
||||
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 +1058,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,12 +1308,21 @@ 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'),
|
||||
(
|
||||
(TYPE_100ME_SFP, 'SFP (100ME)'),
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_2GE_SFP, 'SFP (2.5GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
|
||||
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||
@@ -1333,6 +1352,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 +1371,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)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
@@ -1495,9 +1518,12 @@ class InterfaceSpeedChoices(ChoiceSet):
|
||||
(10000000, '10 Gbps'),
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(50000000, '50 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(200000000, '200 Gbps'),
|
||||
(400000000, '400 Gbps'),
|
||||
(800000000, '800 Gbps'),
|
||||
(1600000000, '1.6 Tbps'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from utilities.forms.fields import (
|
||||
NumericArrayField,
|
||||
SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields, TabbedGroups
|
||||
from utilities.forms.widgets import (
|
||||
APISelect,
|
||||
ClearableFileInput,
|
||||
@@ -142,6 +142,16 @@ class SiteForm(TenancyForm, PrimaryModelForm):
|
||||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
add_asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('Add ASNs'),
|
||||
required=False
|
||||
)
|
||||
remove_asns = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('Remove ASNs'),
|
||||
required=False
|
||||
)
|
||||
slug = SlugField()
|
||||
time_zone = TimeZoneFormField(
|
||||
label=_('Time zone'),
|
||||
@@ -151,7 +161,8 @@ class SiteForm(TenancyForm, PrimaryModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
|
||||
'name', 'slug', 'status', 'region', 'group', 'facility', M2MAddRemoveFields('asns'), 'time_zone',
|
||||
'description', 'tags',
|
||||
name=_('Site')
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
@@ -161,7 +172,7 @@ class SiteForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = (
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
|
||||
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'time_zone',
|
||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags',
|
||||
)
|
||||
widgets = {
|
||||
@@ -177,6 +188,21 @@ class SiteForm(TenancyForm, PrimaryModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD:
|
||||
# Add/remove mode for large M2M sets
|
||||
self.fields.pop('asns')
|
||||
self.fields['add_asns'].widget.add_query_param('site_id__n', self.instance.pk)
|
||||
self.fields['remove_asns'].widget.add_query_param('site_id', self.instance.pk)
|
||||
self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count)
|
||||
else:
|
||||
# Simple mode for new objects or small M2M sets
|
||||
self.fields.pop('add_asns')
|
||||
self.fields.pop('remove_asns')
|
||||
if self.instance.pk:
|
||||
self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True))
|
||||
|
||||
|
||||
class LocationForm(TenancyForm, NestedGroupModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,8 @@ from dcim.choices import (
|
||||
)
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from ipam.models import ASN, RIR, VLAN
|
||||
from utilities.forms.rendering import M2MAddRemoveFields
|
||||
from utilities.testing import create_test_device
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@@ -417,3 +418,111 @@ class InterfaceTestCase(TestCase):
|
||||
self.assertNotIn('untagged_vlan', form.cleaned_data.keys())
|
||||
self.assertNotIn('tagged_vlans', form.cleaned_data.keys())
|
||||
self.assertNotIn('qinq_svlan', form.cleaned_data.keys())
|
||||
|
||||
|
||||
class SiteFormTestCase(TestCase):
|
||||
"""
|
||||
Tests for M2MAddRemoveFields using Site ASN assignments as the test case.
|
||||
Covers both simple mode (single multi-select field) and add/remove mode (dual fields).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.rir = RIR.objects.create(name='RIR 1', slug='rir-1')
|
||||
# Create 110 ASNs: 100 to pre-assign (triggering add/remove mode) plus 10 extras
|
||||
ASN.objects.bulk_create([ASN(asn=i, rir=cls.rir) for i in range(1, 111)])
|
||||
cls.asns = list(ASN.objects.order_by('asn'))
|
||||
|
||||
def _site_data(self, **kwargs):
|
||||
data = {'name': 'Test Site', 'slug': 'test-site', 'status': 'active'}
|
||||
data.update(kwargs)
|
||||
return data
|
||||
|
||||
def test_new_site_uses_simple_mode(self):
|
||||
"""A form for a new site uses the single 'asns' field (simple mode)."""
|
||||
form = SiteForm(data=self._site_data())
|
||||
self.assertIn('asns', form.fields)
|
||||
self.assertNotIn('add_asns', form.fields)
|
||||
self.assertNotIn('remove_asns', form.fields)
|
||||
|
||||
def test_existing_site_below_threshold_uses_simple_mode(self):
|
||||
"""A form for an existing site with fewer than THRESHOLD ASNs uses simple mode."""
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
site.asns.set(self.asns[:5])
|
||||
form = SiteForm(instance=site)
|
||||
self.assertIn('asns', form.fields)
|
||||
self.assertNotIn('add_asns', form.fields)
|
||||
self.assertNotIn('remove_asns', form.fields)
|
||||
|
||||
def test_existing_site_at_threshold_uses_add_remove_mode(self):
|
||||
"""A form for an existing site with THRESHOLD or more ASNs uses add/remove mode."""
|
||||
site = Site.objects.create(name='Site 2', slug='site-2')
|
||||
site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
|
||||
form = SiteForm(instance=site)
|
||||
self.assertNotIn('asns', form.fields)
|
||||
self.assertIn('add_asns', form.fields)
|
||||
self.assertIn('remove_asns', form.fields)
|
||||
|
||||
def test_simple_mode_assigns_asns_on_create(self):
|
||||
"""Saving a new site via simple mode assigns the selected ASNs."""
|
||||
asn_pks = [asn.pk for asn in self.asns[:3]]
|
||||
form = SiteForm(data=self._site_data(asns=asn_pks))
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
site = form.save()
|
||||
self.assertEqual(set(site.asns.values_list('pk', flat=True)), set(asn_pks))
|
||||
|
||||
def test_simple_mode_replaces_asns_on_edit(self):
|
||||
"""Saving an existing site via simple mode replaces the current ASN assignments."""
|
||||
site = Site.objects.create(name='Site 3', slug='site-3')
|
||||
site.asns.set(self.asns[:3])
|
||||
new_asn_pks = [asn.pk for asn in self.asns[3:6]]
|
||||
form = SiteForm(
|
||||
data=self._site_data(name='Site 3', slug='site-3', asns=new_asn_pks),
|
||||
instance=site
|
||||
)
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
site = form.save()
|
||||
self.assertEqual(set(site.asns.values_list('pk', flat=True)), set(new_asn_pks))
|
||||
|
||||
def test_add_remove_mode_adds_asns(self):
|
||||
"""In add/remove mode, specifying 'add_asns' appends to current assignments."""
|
||||
site = Site.objects.create(name='Site 4', slug='site-4')
|
||||
site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
|
||||
new_asn_pks = [asn.pk for asn in self.asns[M2MAddRemoveFields.THRESHOLD:]]
|
||||
form = SiteForm(
|
||||
data=self._site_data(name='Site 4', slug='site-4', add_asns=new_asn_pks),
|
||||
instance=site
|
||||
)
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
site = form.save()
|
||||
self.assertEqual(site.asns.count(), len(self.asns))
|
||||
|
||||
def test_add_remove_mode_removes_asns(self):
|
||||
"""In add/remove mode, specifying 'remove_asns' drops those assignments."""
|
||||
site = Site.objects.create(name='Site 5', slug='site-5')
|
||||
site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
|
||||
remove_pks = [asn.pk for asn in self.asns[:5]]
|
||||
form = SiteForm(
|
||||
data=self._site_data(name='Site 5', slug='site-5', remove_asns=remove_pks),
|
||||
instance=site
|
||||
)
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
site = form.save()
|
||||
self.assertEqual(site.asns.count(), M2MAddRemoveFields.THRESHOLD - 5)
|
||||
self.assertFalse(site.asns.filter(pk__in=remove_pks).exists())
|
||||
|
||||
def test_add_remove_mode_simultaneous_add_and_remove(self):
|
||||
"""In add/remove mode, add and remove operations are applied together."""
|
||||
site = Site.objects.create(name='Site 6', slug='site-6')
|
||||
site.asns.set(self.asns[:M2MAddRemoveFields.THRESHOLD])
|
||||
add_pks = [asn.pk for asn in self.asns[M2MAddRemoveFields.THRESHOLD:M2MAddRemoveFields.THRESHOLD + 3]]
|
||||
remove_pks = [asn.pk for asn in self.asns[:3]]
|
||||
form = SiteForm(
|
||||
data=self._site_data(name='Site 6', slug='site-6', add_asns=add_pks, remove_asns=remove_pks),
|
||||
instance=site
|
||||
)
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
site = form.save()
|
||||
self.assertEqual(site.asns.count(), M2MAddRemoveFields.THRESHOLD)
|
||||
self.assertTrue(site.asns.filter(pk__in=add_pks).count() == 3)
|
||||
self.assertFalse(site.asns.filter(pk__in=remove_pks).exists())
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -510,8 +510,9 @@ class EventRuleTable(NetBoxTable):
|
||||
verbose_name=_('Type'),
|
||||
)
|
||||
action_object = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Object'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
)
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types'),
|
||||
|
||||
24
netbox/extras/tests/test_tables.py
Normal file
24
netbox/extras/tests/test_tables.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.test import RequestFactory, TestCase, tag
|
||||
|
||||
from extras.models import EventRule
|
||||
from extras.tables import EventRuleTable
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class EventRuleTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
rule = EventRule.objects.all()
|
||||
disallowed = {
|
||||
'actions',
|
||||
}
|
||||
|
||||
orderable_columns = [
|
||||
column.name for column in EventRuleTable(rule).columns if column.orderable and column.name not in disallowed
|
||||
]
|
||||
fake_request = RequestFactory().get('/')
|
||||
|
||||
for col in orderable_columns:
|
||||
for direction in ('-', ''):
|
||||
table = EventRuleTable(rule)
|
||||
table.order_by = f'{direction}{col}'
|
||||
table.as_html(fake_request)
|
||||
@@ -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
24
netbox/ipam/ui/attrs.py
Normal 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,
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.fields.related import ManyToManyRel
|
||||
|
||||
from extras.choices import *
|
||||
from utilities.forms.fields import CommentField, SlugField
|
||||
@@ -71,14 +72,49 @@ class NetBoxModelForm(
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
|
||||
Handles both forward and reverse M2M relationships, and supports both simple (single field)
|
||||
and add/remove (dual field) modes.
|
||||
"""
|
||||
self.instance._m2m_values = {}
|
||||
for field in self.instance._meta.local_many_to_many:
|
||||
if field.name in self.cleaned_data:
|
||||
self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
|
||||
|
||||
# Collect names to process: local M2M fields (includes TaggableManager from django-taggit)
|
||||
# plus reverse M2M relations (ManyToManyRel).
|
||||
names = [field.name for field in self.instance._meta.local_many_to_many]
|
||||
names += [
|
||||
field.get_accessor_name()
|
||||
for field in self.instance._meta.get_fields()
|
||||
if isinstance(field, ManyToManyRel)
|
||||
]
|
||||
|
||||
for name in names:
|
||||
if name in self.cleaned_data:
|
||||
# Simple mode: single multi-select field
|
||||
self.instance._m2m_values[name] = list(self.cleaned_data[name])
|
||||
elif f'add_{name}' in self.cleaned_data or f'remove_{name}' in self.cleaned_data:
|
||||
# Add/remove mode: compute the effective set
|
||||
current = set(getattr(self.instance, name).values_list('pk', flat=True)) \
|
||||
if self.instance.pk else set()
|
||||
add_values = set(
|
||||
v.pk for v in self.cleaned_data.get(f'add_{name}', [])
|
||||
)
|
||||
remove_values = set(
|
||||
v.pk for v in self.cleaned_data.get(f'remove_{name}', [])
|
||||
)
|
||||
self.instance._m2m_values[name] = list((current | add_values) - remove_values)
|
||||
|
||||
return super()._post_clean()
|
||||
|
||||
def _save_m2m(self):
|
||||
"""
|
||||
Save many-to-many field values that were computed in _post_clean(). This handles M2M fields
|
||||
not included in Meta.fields (e.g. those managed via M2MAddRemoveFields).
|
||||
"""
|
||||
super()._save_m2m()
|
||||
meta_fields = self._meta.fields
|
||||
for field_name, values in self.instance._m2m_values.items():
|
||||
if not meta_fields or field_name not in meta_fields:
|
||||
getattr(self.instance, field_name).set(values)
|
||||
|
||||
|
||||
class PrimaryModelForm(OwnerMixin, NetBoxModelForm):
|
||||
"""
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
215
netbox/netbox/tests/test_ui.py
Normal file
215
netbox/netbox/tests/test_ui.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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__,
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
@@ -60,7 +60,9 @@
|
||||
"@types/bootstrap/**/@popperjs/core": "^2.11.6",
|
||||
"eslint/**/minimatch": "^3.1.3",
|
||||
"eslint-plugin-import/**/minimatch": "^3.1.3",
|
||||
"**/markdown-it": "^14.1.1"
|
||||
"**/markdown-it": "^14.1.1",
|
||||
"micromatch/picomatch": "2.3.2",
|
||||
"tinyglobby/picomatch": "4.0.4"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
@use 'sass:map';
|
||||
|
||||
// Serialized data from change records
|
||||
pre.change-data {
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
// Remove card-body padding
|
||||
margin-inline: -0.75rem;
|
||||
|
||||
// Display each line individually for highlighting
|
||||
> span {
|
||||
display: block;
|
||||
padding-right: $spacer;
|
||||
padding-left: $spacer;
|
||||
width: 100%;
|
||||
padding-inline: map.get($spacers, 2);
|
||||
max-width: 100%;
|
||||
min-width: fit-content;
|
||||
border-left: map.get($spacers, 1) solid transparent;
|
||||
|
||||
&.added {
|
||||
color: var(--tblr-dark);
|
||||
background-color: $green-300;
|
||||
background-color: var(--tblr-green-200);
|
||||
border-left-color: var(--tblr-green-darken);
|
||||
}
|
||||
|
||||
&.removed {
|
||||
color: var(--tblr-dark);
|
||||
background-color: $red-300;
|
||||
background-color: var(--tblr-red-200);
|
||||
border-left-color: var(--tblr-red-darken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change data diff w/added & removed data
|
||||
pre.change-diff {
|
||||
border-color: transparent;
|
||||
border: var(--tblr-border-width) solid transparent;
|
||||
|
||||
&.change-added {
|
||||
color: var(--tblr-dark);
|
||||
background-color: $green-300;
|
||||
background-color: var(--tblr-green-lt);
|
||||
border-color: var(--tblr-green);
|
||||
}
|
||||
|
||||
&.change-removed {
|
||||
color: var(--tblr-dark);
|
||||
background-color: $red-300;
|
||||
background-color: var(--tblr-red-lt);
|
||||
border-color: var(--tblr-red);
|
||||
}
|
||||
}
|
||||
|
||||
// <pre> elements displayed with a border
|
||||
pre.block {
|
||||
padding: $spacer;
|
||||
border: 1px solid $border-color;
|
||||
border: var(--tblr-border-width) solid $border-color;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
@@ -2076,9 +2076,9 @@ flatpickr@4.6.13:
|
||||
integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
|
||||
|
||||
flatted@^3.2.9:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
version "3.4.2"
|
||||
resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz"
|
||||
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
@@ -2993,15 +2993,25 @@ path-parse@^1.0.7:
|
||||
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
picomatch@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
|
||||
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
||||
|
||||
picomatch@4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
|
||||
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
||||
|
||||
picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
possible-typed-array-names@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.5.5"
|
||||
version: "4.5.6"
|
||||
edition: "Community"
|
||||
published: "2026-03-17"
|
||||
published: "2026-03-31"
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
2
netbox/templates/circuits/circuit/attrs/commit_rate.html
Normal file
2
netbox/templates/circuits/circuit/attrs/commit_rate.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{{ value|humanize_speed }}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}"> </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 %}
|
||||
|
||||
@@ -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 }}
|
||||
<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 }}
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
16
netbox/templates/circuits/panels/circuit_termination.html
Normal file
16
netbox/templates/circuits/panels/circuit_termination.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}"> </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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
1
netbox/templates/core/datafile/attrs/size.html
Normal file
1
netbox/templates/core/datafile/attrs/size.html
Normal file
@@ -0,0 +1 @@
|
||||
{% load i18n %}{{ value }} {% trans "bytes" %}
|
||||
@@ -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 %}
|
||||
|
||||
1
netbox/templates/core/datasource/attrs/ignore_rules.html
Normal file
1
netbox/templates/core/datasource/attrs/ignore_rules.html
Normal file
@@ -0,0 +1 @@
|
||||
<pre>{{ value }}</pre>
|
||||
1
netbox/templates/core/datasource/attrs/source_url.html
Normal file
1
netbox/templates/core/datasource/attrs/source_url.html
Normal file
@@ -0,0 +1 @@
|
||||
{% if not object.type.is_local %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value }}{% endif %}
|
||||
@@ -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 %}
|
||||
|
||||
1
netbox/templates/core/job/attrs/object_type.html
Normal file
1
netbox/templates/core/job/attrs/object_type.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ value }}</a>
|
||||
3
netbox/templates/core/job/attrs/scheduled.html
Normal file
3
netbox/templates/core/job/attrs/scheduled.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
{{ value|isodatetime }}{% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}){% endif %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% if object.changed_object and object.changed_object.get_absolute_url %}{{ object.changed_object|linkify }}{% else %}{{ value }}{% endif %}
|
||||
1
netbox/templates/core/objectchange/attrs/request_id.html
Normal file
1
netbox/templates/core/objectchange/attrs/request_id.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
|
||||
1
netbox/templates/core/objectchange/attrs/user.html
Normal file
1
netbox/templates/core/objectchange/attrs/user.html
Normal file
@@ -0,0 +1 @@
|
||||
{% if object.user and object.user.get_full_name %}{{ object.user.get_full_name }} ({{ value }}){% else %}{{ value }}{% endif %}
|
||||
11
netbox/templates/core/panels/configrevision_comment.html
Normal file
11
netbox/templates/core/panels/configrevision_comment.html
Normal 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">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
5
netbox/templates/core/panels/configrevision_data.html
Normal file
5
netbox/templates/core/panels/configrevision_data.html
Normal 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>
|
||||
8
netbox/templates/core/panels/datafile_content.html
Normal file
8
netbox/templates/core/panels/datafile_content.html
Normal 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 %}
|
||||
26
netbox/templates/core/panels/datasource_backend.html
Normal file
26
netbox/templates/core/panels/datasource_backend.html
Normal 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 %}
|
||||
31
netbox/templates/core/panels/objectchange_difference.html
Normal file
31
netbox/templates/core/panels/objectchange_difference.html
Normal 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="card-actions">
|
||||
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-ghost-primary btn-sm">
|
||||
<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-ghost-primary btn-sm">
|
||||
{% 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>
|
||||
18
netbox/templates/core/panels/objectchange_postchange.html
Normal file
18
netbox/templates/core/panels/objectchange_postchange.html
Normal 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>
|
||||
20
netbox/templates/core/panels/objectchange_prechange.html
Normal file
20
netbox/templates/core/panels/objectchange_prechange.html
Normal 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>
|
||||
11
netbox/templates/core/panels/objectchange_related.html
Normal file
11
netbox/templates/core/panels/objectchange_related.html
Normal 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 %}
|
||||
@@ -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 }}"> </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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}"> </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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
3
netbox/templates/dcim/interface/attrs/mac_address.html
Normal file
3
netbox/templates/dcim/interface/attrs/mac_address.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% load helpers i18n %}
|
||||
<span class="font-monospace">{{ value|linkify }}</span>
|
||||
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||
2
netbox/templates/dcim/interface/attrs/speed.html
Normal file
2
netbox/templates/dcim/interface/attrs/speed.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{{ value|humanize_speed }}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}"> </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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
6
netbox/templates/dcim/panels/cable_termination_a.html
Normal file
6
netbox/templates/dcim/panels/cable_termination_a.html
Normal 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 %}
|
||||
6
netbox/templates/dcim/panels/cable_termination_b.html
Normal file
6
netbox/templates/dcim/panels/cable_termination_b.html
Normal 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 %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user