mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
efb41b7433
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
|||||||
What version of NetBox are you currently running? (If you don't have access to the most
|
What version of NetBox are you currently running? (If you don't have access to the most
|
||||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||||
before opening a bug report to see if your issue has already been addressed.)
|
before opening a bug report to see if your issue has already been addressed.)
|
||||||
placeholder: v3.0.7
|
placeholder: v3.0.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.0.7
|
placeholder: v3.0.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1 +0,0 @@
|
|||||||
{!models/extras/customlink.md!}
|
|
@ -1,6 +1,27 @@
|
|||||||
# NetBox v3.0
|
# NetBox v3.0
|
||||||
|
|
||||||
## v3.0.8 (FUTURE)
|
## v3.0.9 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.0.8 (2021-10-20)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind
|
||||||
|
* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring
|
||||||
|
* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap
|
||||||
|
* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports
|
||||||
|
* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession
|
||||||
|
* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects
|
||||||
|
* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks
|
||||||
|
* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records
|
||||||
|
* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available
|
||||||
|
* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ nav:
|
|||||||
- Customization:
|
- Customization:
|
||||||
- Custom Fields: 'customization/custom-fields.md'
|
- Custom Fields: 'customization/custom-fields.md'
|
||||||
- Custom Validation: 'customization/custom-validation.md'
|
- Custom Validation: 'customization/custom-validation.md'
|
||||||
- Custom Links: 'customization/custom-links.md'
|
- Custom Links: 'models/extras/customlink.md'
|
||||||
- Export Templates: 'customization/export-templates.md'
|
- Export Templates: 'customization/export-templates.md'
|
||||||
- Custom Scripts: 'customization/custom-scripts.md'
|
- Custom Scripts: 'customization/custom-scripts.md'
|
||||||
- Reports: 'customization/reports.md'
|
- Reports: 'customization/reports.md'
|
||||||
|
@ -704,6 +704,18 @@ class PowerOutletFeedLegChoices(ChoiceSet):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class InterfaceKindChoices(ChoiceSet):
|
||||||
|
KIND_PHYSICAL = 'physical'
|
||||||
|
KIND_VIRTUAL = 'virtual'
|
||||||
|
KIND_WIRELESS = 'wireless'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(KIND_PHYSICAL, 'Physical'),
|
||||||
|
(KIND_VIRTUAL, 'Virtual'),
|
||||||
|
(KIND_WIRELESS, 'Wireless'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTypeChoices(ChoiceSet):
|
class InterfaceTypeChoices(ChoiceSet):
|
||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
|
@ -7,7 +7,6 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from tenancy.models import Tenant
|
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
@ -966,9 +965,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
|
||||||
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'],
|
||||||
]
|
]
|
||||||
|
kind = forms.MultipleChoiceField(
|
||||||
|
choices=InterfaceKindChoices,
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelectMultiple()
|
||||||
|
)
|
||||||
type = forms.MultipleChoiceField(
|
type = forms.MultipleChoiceField(
|
||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -15,6 +15,7 @@ from .models import *
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextFilterSet',
|
'ConfigContextFilterSet',
|
||||||
'ContentTypeFilterSet',
|
'ContentTypeFilterSet',
|
||||||
|
'CustomFieldFilterSet',
|
||||||
'CustomLinkFilterSet',
|
'CustomLinkFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
'ImageAttachmentFilterSet',
|
'ImageAttachmentFilterSet',
|
||||||
@ -47,7 +48,7 @@ class WebhookFilterSet(BaseFilterSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
class CustomFieldFilterSet(BaseFilterSet):
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -260,11 +260,16 @@ class IPRangeTable(BaseTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
|
utilization = UtilizationColumn(
|
||||||
|
accessor='utilization',
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||||
|
'utilization',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.0.8-dev'
|
VERSION = '3.0.9-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -137,7 +137,7 @@ class HomeView(View):
|
|||||||
release_version, release_url = latest_release
|
release_version, release_url = latest_release
|
||||||
if release_version > version.parse(settings.VERSION):
|
if release_version > version.parse(settings.VERSION):
|
||||||
new_release = {
|
new_release = {
|
||||||
'version': str(latest_release),
|
'version': str(release_version),
|
||||||
'url': release_url,
|
'url': release_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,11 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
messages.success(request, mark_safe(msg))
|
messages.success(request, mark_safe(msg))
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
redirect_url = request.get_full_path()
|
redirect_url = request.path
|
||||||
|
|
||||||
# If the object has clone_fields, pre-populate a new instance of the form
|
# If the object has clone_fields, pre-populate a new instance of the form
|
||||||
if hasattr(obj, 'clone_fields'):
|
if hasattr(obj, 'clone_fields'):
|
||||||
redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}"
|
redirect_url += f"?{prepare_cloned_fields(obj)}"
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,6 +1,17 @@
|
|||||||
import { createToast } from '../bs';
|
import { createToast } from '../bs';
|
||||||
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
||||||
|
|
||||||
|
// Match an interface name that begins with a capital letter and is followed by at least one other
|
||||||
|
// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
|
||||||
|
const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
|
||||||
|
|
||||||
|
// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
|
||||||
|
// the first two characters).
|
||||||
|
const CISCO_IOS_OVERRIDES = new Map<string, string>([
|
||||||
|
// Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
|
||||||
|
['TwentyFiveGigE', 'Twe'],
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an attribute from a row's cell.
|
* Get an attribute from a row's cell.
|
||||||
*
|
*
|
||||||
@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string
|
|||||||
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
|
||||||
|
* interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
|
||||||
|
* would become `Gi0/1/2`.
|
||||||
|
*
|
||||||
|
* This should probably be replaced with something in the primary application (Django), such as
|
||||||
|
* a database field attached to given interface types. However, this is a temporary measure to
|
||||||
|
* replace the functionality of this one-liner:
|
||||||
|
*
|
||||||
|
* @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
|
||||||
|
*
|
||||||
|
* @param name Long-form/original interface name.
|
||||||
|
*/
|
||||||
|
function getInterfaceAlias(name: string | null): string | null {
|
||||||
|
if (name === null) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (name.match(CISCO_IOS_PATTERN)) {
|
||||||
|
// Extract the base name and numeric portions of the interface. For example, an input interface
|
||||||
|
// of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
|
||||||
|
const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
|
||||||
|
|
||||||
|
if (isTruthy(base) && isTruthy(numeric)) {
|
||||||
|
// Check the override map and use its value if the base name is present in the map.
|
||||||
|
// Otherwise, use the first two characters of the base name. For example,
|
||||||
|
// `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
|
||||||
|
// `Twe0/0/1`.
|
||||||
|
const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
|
||||||
|
return `${aliasBase}${numeric}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update row styles based on LLDP neighbor data.
|
* Update row styles based on LLDP neighbor data.
|
||||||
*/
|
*/
|
||||||
@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) {
|
|||||||
|
|
||||||
if (row !== null) {
|
if (row !== null) {
|
||||||
for (const neighbor of neighbors) {
|
for (const neighbor of neighbors) {
|
||||||
const cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
|
const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
|
||||||
const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
|
const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
|
||||||
const cDevice = getData(row, 'td.configured_device', 'data');
|
const configuredDevice = getData(row, 'td.configured_device', 'data');
|
||||||
const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
||||||
const cInterface = getData(row, 'td.configured_interface', 'data');
|
const configuredIface = getData(row, 'td.configured_interface', 'data');
|
||||||
|
|
||||||
let cInterfaceShort = null;
|
const interfaceAlias = getInterfaceAlias(configuredIface);
|
||||||
if (isTruthy(cInterface)) {
|
|
||||||
cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2');
|
const remoteName = neighbor.remote_system_name ?? '';
|
||||||
|
const remotePort = neighbor.remote_port ?? '';
|
||||||
|
const [neighborDevice] = remoteName.split('.');
|
||||||
|
const [neighborIface] = remotePort.split('.');
|
||||||
|
|
||||||
|
if (deviceCell !== null) {
|
||||||
|
deviceCell.innerText = neighborDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nHost = neighbor.remote_system_name ?? '';
|
if (interfaceCell !== null) {
|
||||||
const nPort = neighbor.remote_port ?? '';
|
interfaceCell.innerText = neighborIface;
|
||||||
const [nDevice] = nHost.split('.');
|
|
||||||
const [nInterface] = nPort.split('.');
|
|
||||||
|
|
||||||
if (cellDevice !== null) {
|
|
||||||
cellDevice.innerText = nDevice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cellInterface !== null) {
|
// Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
|
||||||
cellInterface.innerText = nInterface;
|
const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTruthy(cDevice) && isTruthy(nDevice)) {
|
// NetBox device or chassis matches LLDP neighbor.
|
||||||
|
const validNode =
|
||||||
|
configuredDevice === neighborDevice || configuredChassis === neighborDevice;
|
||||||
|
|
||||||
|
// NetBox configured interface matches LLDP neighbor interface.
|
||||||
|
const validInterface =
|
||||||
|
configuredIface === neighborIface || interfaceAlias === neighborIface;
|
||||||
|
|
||||||
|
if (nonConfiguredDevice) {
|
||||||
row.classList.add('info');
|
row.classList.add('info');
|
||||||
} else if (
|
} else if (validNode && validInterface) {
|
||||||
(cDevice === nDevice || cChassis === nDevice) &&
|
|
||||||
cInterfaceShort === nInterface
|
|
||||||
) {
|
|
||||||
row.classList.add('success');
|
|
||||||
} else if (cDevice === nDevice || cChassis === nDevice) {
|
|
||||||
row.classList.add('success');
|
row.classList.add('success');
|
||||||
} else {
|
} else {
|
||||||
row.classList.add('danger');
|
row.classList.add('danger');
|
||||||
|
@ -266,10 +266,8 @@ class SideNav {
|
|||||||
for (const link of this.getActiveLinks()) {
|
for (const link of this.getActiveLinks()) {
|
||||||
this.activateLink(link, 'collapse');
|
this.activateLink(link, 'collapse');
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
this.bodyRemove('hide');
|
||||||
this.bodyRemove('hide');
|
this.bodyAdd('hidden');
|
||||||
this.bodyAdd('hidden');
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,9 +197,15 @@ table {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dropdown {
|
||||||
|
// Presence of 'overflow: scroll' on a table causes dropdowns to be improperly hidden when
|
||||||
|
// opened. See: https://github.com/twbs/bootstrap/issues/24251
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
a, a:hover {
|
a,
|
||||||
|
a:hover {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,11 @@
|
|||||||
// Navbar brand
|
// Navbar brand
|
||||||
.sidenav-brand {
|
.sidenav-brand {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav-brand-icon {
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-inner {
|
.sidenav-inner {
|
||||||
@ -141,7 +146,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-toggle {
|
.sidenav-toggle {
|
||||||
display: none;
|
// The sidenav toggle's default state is "hidden". Because modifying the `display` property
|
||||||
|
// isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute
|
||||||
|
// to yield a similar result.
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0;
|
||||||
|
// The transition itself is largely irrelevant, but CSS needs *something* to transition in
|
||||||
|
// order to apply a delay.
|
||||||
|
transition: opacity 10ms ease-in-out;
|
||||||
|
// Offset the transition delay so the icon isn't visible during the logo transition.
|
||||||
|
transition-delay: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-collapse {
|
.sidenav-collapse {
|
||||||
@ -350,13 +365,21 @@
|
|||||||
.sidenav-brand {
|
.sidenav-brand {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-150%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav-brand-icon {
|
.sidenav-brand-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidenav-toggle {
|
||||||
|
// Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap
|
||||||
|
// with the logo elements.
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
transition: unset;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-nav > .nav-item {
|
.navbar-nav > .nav-item {
|
||||||
> .nav-link {
|
> .nav-link {
|
||||||
&:after {
|
&:after {
|
||||||
@ -402,7 +425,8 @@
|
|||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
@include media-breakpoint-up(lg) {
|
||||||
.sidenav-toggle {
|
.sidenav-toggle {
|
||||||
display: inline-block;
|
position: relative;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300;
|
|||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
$component-active-bg: $primary;
|
$component-active-bg: $primary;
|
||||||
|
$component-active-color: $black;
|
||||||
$form-text-color: $text-muted;
|
$form-text-color: $text-muted;
|
||||||
$input-bg: $gray-900;
|
$input-bg: $gray-900;
|
||||||
$input-disabled-bg: $gray-700;
|
$input-disabled-bg: $gray-700;
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<ul class="nav nav-pills" role="tablist">
|
<ul class="nav nav-pills mb-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
|
<button class="nav-link active" data-bs-target="#interfaces" role="tab" data-bs-toggle="tab">
|
||||||
Interfaces {% badge interface_table.rows|length %}
|
Interfaces {% badge interface_table.rows|length %}
|
||||||
|
@ -130,12 +130,12 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if object.postchange_data %}
|
{% if object.postchange_data %}
|
||||||
<pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
|
<pre class="change-data">{% for k, v in object.postchange_data.items %}{% spaceless %}
|
||||||
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
|
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|render_json }}</span>
|
||||||
{% endspaceless %}{% endfor %}
|
{% endspaceless %}{% endfor %}
|
||||||
</pre>
|
</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Update</th>
|
<th scope="row">Update</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.type_create %}
|
{% if object.type_update %}
|
||||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
@ -57,7 +57,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Delete</th>
|
<th scope="row">Delete</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.type_create %}
|
{% if object.type_delete %}
|
||||||
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
<i class="mdi mdi-close-thick text-danger" title="No"></i>
|
||||||
|
@ -6,9 +6,17 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{# Breadcrumbs #}
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
{# Breadcrumbs #}
|
||||||
<div class="float-end">
|
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||||
|
{% endblock breadcrumbs %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{# Object identifier #}
|
||||||
|
<div class="float-end px-3">
|
||||||
<code class="text-muted">
|
<code class="text-muted">
|
||||||
{% block object_identifier %}
|
{% block object_identifier %}
|
||||||
{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
|
{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
|
||||||
@ -16,12 +24,7 @@
|
|||||||
{% endblock object_identifier %}
|
{% endblock object_identifier %}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<ol class="breadcrumb">
|
</div>
|
||||||
{% block breadcrumbs %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
|
||||||
{% endblock breadcrumbs %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
{% load django_tables2 %}
|
{% load django_tables2 %}
|
||||||
|
|
||||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
<div class="table-responsive">
|
||||||
|
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||||
{% if table.show_header %}
|
{% if table.show_header %}
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in table.columns %}
|
{% for column in table.columns %}
|
||||||
{% if column.orderable %}
|
{% if column.orderable %}
|
||||||
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in table.page.object_list|default:table.rows %}
|
{% for row in table.page.object_list|default:table.rows %}
|
||||||
<tr {{ row.attrs.as_html }}>
|
<tr {{ row.attrs.as_html }}>
|
||||||
{% for column, cell in row.items %}
|
{% for column, cell in row.items %}
|
||||||
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
|
<td {{ column.attrs.td.as_html }}>{{ cell }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
{% if table.empty_text %}
|
{% if table.empty_text %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{ table.columns|length }}" class="text-center text-muted">— {{ table.empty_text }} —</td>
|
<td colspan="{{ table.columns|length }}" class="text-center text-muted">— {{ table.empty_text }} —</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
{% if table.has_footer %}
|
{% if table.has_footer %}
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in table.columns %}
|
{% for column in table.columns %}
|
||||||
<td>{{ column.footer }}</td>
|
<td>{{ column.footer }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
@ -58,7 +58,7 @@ def render_json(value):
|
|||||||
"""
|
"""
|
||||||
Render a dictionary as formatted JSON.
|
Render a dictionary as formatted JSON.
|
||||||
"""
|
"""
|
||||||
return json.dumps(value, indent=4, sort_keys=True)
|
return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
|
@ -18,11 +18,11 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.2
|
Jinja2==3.0.2
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
markdown-include==0.6.0
|
markdown-include==0.6.0
|
||||||
mkdocs-material==7.3.2
|
mkdocs-material==7.3.4
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.3.2
|
Pillow==8.4.0
|
||||||
psycopg2-binary==2.9.1
|
psycopg2-binary==2.9.1
|
||||||
PyYAML==5.4.1
|
PyYAML==6.0
|
||||||
svgwrite==1.4.1
|
svgwrite==1.4.1
|
||||||
tablib==3.0.0
|
tablib==3.0.0
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user