/edit/', views.DeviceEditView.as_view(), name='device_edit'),
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 0bdca686d..966d90876 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1784,6 +1784,12 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceTable
+class DeviceBulkRenameView(generic.BulkRenameView):
+ queryset = Device.objects.all()
+ filterset = filtersets.DeviceFilterSet
+ table = tables.DeviceTable
+
+
#
# Devices
#
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index b7d77e550..55b7a9f03 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from utilities import filters
from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
- LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
+ JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
)
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
@@ -343,7 +343,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
- field = forms.JSONField(required=required, initial=initial)
+ field = JSONField(required=required, initial=initial)
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 12ab44399..84e93be25 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
-VERSION = '3.2.8'
+VERSION = '3.2.9'
# Hostname
HOSTNAME = platform.node()
diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
index f78b9f37c..6ab50d4c2 100644
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -14,6 +14,7 @@ from django_tables2.columns import library
from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices
+from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, get_viewname
__all__ = (
@@ -427,7 +428,7 @@ class CustomFieldColumn(tables.Column):
super().__init__(*args, **kwargs)
@staticmethod
- def _likify_item(item):
+ def _linkify_item(item):
if hasattr(item, 'get_absolute_url'):
return f'{escape(item)}'
return escape(item)
@@ -443,11 +444,13 @@ class CustomFieldColumn(tables.Column):
return ', '.join(v for v in value)
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
return mark_safe(', '.join(
- self._likify_item(obj) for obj in self.customfield.deserialize(value)
+ self._linkify_item(obj) for obj in self.customfield.deserialize(value)
))
+ if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value:
+ return render_markdown(value)
if value is not None:
obj = self.customfield.deserialize(value)
- return mark_safe(self._likify_item(obj))
+ return mark_safe(self._linkify_item(obj))
return self.default
def value(self, value):
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index 7e07c57d0..5aea9c469 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -633,7 +633,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
replace = form.cleaned_data['replace']
if form.cleaned_data['use_regex']:
try:
- obj.new_name = re.sub(find, replace, obj.name)
+ obj.new_name = re.sub(find, replace, obj.name or '')
# Catch regex group reference errors
except re.error:
obj.new_name = obj.name
diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css
index b929f176a..94718ac40 100644
Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ
diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css
index 341369adf..bf0ba2f62 100644
Binary files a/netbox/project-static/dist/netbox-light.css and b/netbox/project-static/dist/netbox-light.css differ
diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css
index 61bcedf9c..4ccd41d8e 100644
Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ
diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js
index 9ea2e5c7c..5ab9da845 100644
Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ
diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map
index 60656bc6d..9b92d1489 100644
Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ
diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts
index 9e8a31c5b..97fe1826a 100644
--- a/netbox/project-static/src/search.ts
+++ b/netbox/project-static/src/search.ts
@@ -27,6 +27,23 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement): voi
}
}
+/**
+ * Show/hide quicksearch clear button.
+ *
+ * @param event "keyup" or "search" event for the quicksearch input
+ */
+function quickSearchEventHandler(event: Event): void {
+ const quicksearch = event.currentTarget as HTMLInputElement;
+ const inputgroup = quicksearch.parentElement as HTMLDivElement;
+ if (isTruthy(inputgroup)) {
+ if (quicksearch.value === "") {
+ inputgroup.classList.add("hide-last-child");
+ } else {
+ inputgroup.classList.remove("hide-last-child");
+ }
+ }
+}
+
/**
* Initialize Search Bar Elements.
*/
@@ -40,8 +57,35 @@ function initSearchBar(): void {
}
}
+/**
+ * Initialize Quicksearch Event listener/handlers.
+ */
+function initQuickSearch(): void {
+ const quicksearch = document.getElementById("quicksearch") as HTMLInputElement;
+ const clearbtn = document.getElementById("quicksearch_clear") as HTMLButtonElement;
+ if (isTruthy(quicksearch)) {
+ quicksearch.addEventListener("keyup", quickSearchEventHandler, {
+ passive: true
+ })
+ quicksearch.addEventListener("search", quickSearchEventHandler, {
+ passive: true
+ })
+ if (isTruthy(clearbtn)) {
+ clearbtn.addEventListener("click", async () => {
+ const search = new Event('search');
+ quicksearch.value = '';
+ await new Promise(f => setTimeout(f, 100));
+ quicksearch.dispatchEvent(search);
+ }, {
+ passive: true
+ })
+ }
+ }
+}
+
export function initSearch(): void {
for (const func of [initSearchBar]) {
func();
}
+ initQuickSearch();
}
diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss
index a54b6c324..d78e9e9b9 100644
--- a/netbox/project-static/styles/netbox.scss
+++ b/netbox/project-static/styles/netbox.scss
@@ -416,6 +416,27 @@ nav.search {
}
}
+// Styles for the quicksearch and its clear button;
+// Overrides input-group styles and adds transition effects
+.quicksearch {
+ input[type="search"] {
+ border-radius: $border-radius !important;
+ }
+
+ button {
+ margin-left: -32px !important;
+ z-index: 100 !important;
+ outline: none !important;
+ border-radius: $border-radius !important;
+ transition: visibility 0s, opacity 0.2s linear;
+ }
+
+ button :hover {
+ opacity: 50%;
+ transition: visibility 0s, opacity 0.1s linear;
+ }
+}
+
main.layout {
display: flex;
flex-wrap: nowrap;
@@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
height: 18rem;
}
-textarea#id_local_context_data,
textarea.markdown,
-textarea#id_public_key,
-textarea.form-control[name='csv'],
-textarea.form-control[name='data'] {
+textarea.form-control[name='csv'] {
font-family: $font-family-monospace;
}
diff --git a/netbox/project-static/styles/overrides.scss b/netbox/project-static/styles/overrides.scss
index 03c72c6e6..7fa366df8 100644
--- a/netbox/project-static/styles/overrides.scss
+++ b/netbox/project-static/styles/overrides.scss
@@ -34,3 +34,11 @@ a[type='button'] {
.badge {
font-size: $font-size-xs;
}
+
+/* clears the 'X' in search inputs from webkit browsers */
+input[type='search']::-webkit-search-decoration,
+input[type='search']::-webkit-search-cancel-button,
+input[type='search']::-webkit-search-results-button,
+input[type='search']::-webkit-search-results-decoration {
+ -webkit-appearance: none !important;
+}
diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss
index c0933e991..4bbe5cea5 100644
--- a/netbox/project-static/styles/theme-dark.scss
+++ b/netbox/project-static/styles/theme-dark.scss
@@ -92,6 +92,10 @@ $input-focus-color: $input-color;
$input-placeholder-color: $gray-700;
$input-plaintext-color: $body-color;
+input {
+ color-scheme: dark;
+}
+
$form-check-input-active-filter: brightness(90%);
$form-check-input-bg: $input-bg;
$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);
diff --git a/netbox/project-static/styles/theme-light.scss b/netbox/project-static/styles/theme-light.scss
index d417e1bf6..c9478f1cc 100644
--- a/netbox/project-static/styles/theme-light.scss
+++ b/netbox/project-static/styles/theme-light.scss
@@ -22,7 +22,6 @@ $theme-colors: (
'danger': $danger,
'light': $light,
'dark': $dark,
-
// General-purpose palette
'blue': $blue-500,
'indigo': $indigo-500,
@@ -36,7 +35,7 @@ $theme-colors: (
'cyan': $cyan-500,
'gray': $gray-500,
'black': $black,
- 'white': $white,
+ 'white': $white
);
$light: $gray-200;
diff --git a/netbox/project-static/styles/utilities.scss b/netbox/project-static/styles/utilities.scss
index cd8ccc46b..a5a4bf038 100644
--- a/netbox/project-static/styles/utilities.scss
+++ b/netbox/project-static/styles/utilities.scss
@@ -42,3 +42,9 @@ table td {
visibility: visible !important;
}
}
+
+// Hides the last child of an element
+.hide-last-child :last-child {
+ visibility: hidden;
+ opacity: 0;
+}
diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html
index afc306bd4..6f8b383c3 100644
--- a/netbox/templates/dcim/device/consoleports.html
+++ b/netbox/templates/dcim/device/consoleports.html
@@ -16,31 +16,37 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html
index 5f244cdc7..f246d4a82 100644
--- a/netbox/templates/dcim/device/consoleserverports.html
+++ b/netbox/templates/dcim/device/consoleserverports.html
@@ -16,31 +16,37 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html
index 5e33bdae0..d84408962 100644
--- a/netbox/templates/dcim/device/devicebays.html
+++ b/netbox/templates/dcim/device/devicebays.html
@@ -16,28 +16,30 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html
index 0d0f9577c..513d02090 100644
--- a/netbox/templates/dcim/device/frontports.html
+++ b/netbox/templates/dcim/device/frontports.html
@@ -16,31 +16,37 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device/inc/interface_table_controls.html b/netbox/templates/dcim/device/inc/interface_table_controls.html
new file mode 100644
index 000000000..14e552439
--- /dev/null
+++ b/netbox/templates/dcim/device/inc/interface_table_controls.html
@@ -0,0 +1,11 @@
+{% extends 'inc/table_controls_htmx.html' %}
+
+{% block extra_table_controls %}
+
+
+{% endblock extra_table_controls %}
diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html
index 22f6d8be5..2019d9135 100644
--- a/netbox/templates/dcim/device/interfaces.html
+++ b/netbox/templates/dcim/device/interfaces.html
@@ -4,84 +4,63 @@
{% load static %}
{% block content %}
-
-
-
-
- {% if request.user.is_authenticated %}
-
- {% endif %}
-
-
-
+ {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
+
+
{% endblock %}
diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html
index cf71e81ba..cd8597e63 100644
--- a/netbox/templates/dcim/device/powerports.html
+++ b/netbox/templates/dcim/device/powerports.html
@@ -16,31 +16,37 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html
index 73341990f..b370de189 100644
--- a/netbox/templates/dcim/device/rearports.html
+++ b/netbox/templates/dcim/device/rearports.html
@@ -16,31 +16,37 @@
{% endblock %}
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html
index 60efc842e..50af50525 100644
--- a/netbox/templates/dcim/device_list.html
+++ b/netbox/templates/dcim/device_list.html
@@ -1,4 +1,5 @@
{% extends 'generic/object_list.html' %}
+{% load buttons %}
{% block bulk_buttons %}
{% if perms.dcim.change_device %}
@@ -73,5 +74,15 @@
{% endif %}
- {{ block.super }}
+ {% if 'bulk_edit' in actions %}
+
+ {% bulk_edit_button model query_params=request.GET %}
+
+
+ {% endif %}
+ {% if 'bulk_delete' in actions %}
+ {% bulk_delete_button model query_params=request.GET %}
+ {% endif %}
{% endblock %}
diff --git a/netbox/templates/inc/panels/contacts.html b/netbox/templates/inc/panels/contacts.html
index 26961f04a..359ad8d7e 100644
--- a/netbox/templates/inc/panels/contacts.html
+++ b/netbox/templates/inc/panels/contacts.html
@@ -10,6 +10,8 @@
Name |
Role |
Priority |
+ Phone |
+ Email |
|
{% for contact in contacts %}
@@ -17,6 +19,20 @@
{{ contact.contact|linkify }} |
{{ contact.role|placeholder }} |
{{ contact.get_priority_display|placeholder }} |
+
+ {% if contact.contact.phone %}
+ {{ contact.contact.phone }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+ {% if contact.contact.email %}
+ {{ contact.contact.email }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
{% if perms.tenancy.change_contactassignment %}
diff --git a/netbox/templates/inc/table_controls_htmx.html b/netbox/templates/inc/table_controls_htmx.html
index ab8167bc0..6d6926d5d 100644
--- a/netbox/templates/inc/table_controls_htmx.html
+++ b/netbox/templates/inc/table_controls_htmx.html
@@ -1,29 +1,19 @@
{% load helpers %}
- |
+ {% if object.prefix.version == 4 %}
+
+ {% endif %}
{% include 'inc/panels/custom_fields.html' %}
@@ -161,3 +168,39 @@
{% endblock %}
+
+{% block modals %}
+ {{ block.super }}
+ {% if object.prefix.version == 4 %}
+
+
+
+
+
+
+
+ Network Address |
+ {{ object.prefix.network }} |
+
+
+ Network Mask |
+ {{ object.prefix.netmask }} |
+
+
+ Wildcard Mask |
+ {{ object.prefix.hostmask }} |
+
+
+ Broadcast Address |
+ {{ object.prefix.broadcast }} |
+
+
+
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html
index e3ffb84d4..eff98cdd6 100644
--- a/netbox/templates/virtualization/virtualmachine/interfaces.html
+++ b/netbox/templates/virtualization/virtualmachine/interfaces.html
@@ -15,27 +15,28 @@
- {% if perms.virtualization.change_vminterface %}
-
-
- {% endif %}
- {% if perms.virtualization.delete_vminterface %}
-
- {% endif %}
- {% if perms.virtualization.add_vminterface %}
-
- {% endif %}
-
+ {% if perms.virtualization.change_vminterface %}
+
+
+
+
+ {% endif %}
+ {% if perms.virtualization.delete_vminterface %}
+
+ {% endif %}
+ {% if perms.virtualization.add_vminterface %}
+
+ {% endif %}
{% endblock content %}
diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py
index 9168189a1..df69339e5 100644
--- a/netbox/utilities/forms/fields/fields.py
+++ b/netbox/utilities/forms/fields/fields.py
@@ -99,6 +99,7 @@ class JSONField(_JSONField):
if not self.help_text:
self.help_text = 'Enter context data in JSON format.'
self.widget.attrs['placeholder'] = ''
+ self.widget.attrs['class'] = 'font-monospace'
def prepare_value(self, value):
if isinstance(value, InvalidJSONInput):
diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py
index 3b5cd8308..8ad6f103b 100644
--- a/netbox/utilities/forms/forms.py
+++ b/netbox/utilities/forms/forms.py
@@ -136,7 +136,7 @@ class ImportForm(BootstrapMixin, forms.Form):
Generic form for creating an object from JSON/YAML data
"""
data = forms.CharField(
- widget=forms.Textarea,
+ widget=forms.Textarea(attrs={'class': 'font-monospace'}),
help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
)
format = forms.ChoiceField(
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py
index 88aa1a6c2..2f52850bd 100644
--- a/netbox/virtualization/forms/filtersets.py
+++ b/netbox/virtualization/forms/filtersets.py
@@ -89,7 +89,7 @@ class VirtualMachineFilterForm(
(None, ('q', 'tag')),
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')),
('Location', ('region_id', 'site_group_id', 'site_id')),
- ('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
+ ('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
diff --git a/requirements.txt b/requirements.txt
index 59bd1e8cd..d99e09943 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,13 +19,13 @@ gunicorn==20.1.0
Jinja2==3.1.2
Markdown==3.4.1
markdown-include==0.7.0
-mkdocs-material==8.3.9
+mkdocs-material==8.4.0
mkdocstrings[python-legacy]==0.19.0
netaddr==0.8.0
Pillow==9.2.0
psycopg2-binary==2.9.3
PyYAML==6.0
-sentry-sdk==1.9.2
+sentry-sdk==1.9.5
social-auth-app-django==5.0.0
social-auth-core==4.3.0
svgwrite==1.4.3
@@ -34,3 +34,6 @@ tzdata==2022.1
# Workaround for #7401
jsonschema==3.2.0
+
+# Workaround for #9986
+pytz==2022.1