Merge branch 'develop' into feature

This commit is contained in:
jeremystretch
2022-08-12 10:18:57 -04:00
21 changed files with 214 additions and 114 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
## Front Ports ## Front Ports
Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports, using numeric positions to annotate the specific alignment of each. Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each.
+10
View File
@@ -2,6 +2,16 @@
## v3.2.9 (FUTURE) ## v3.2.9 (FUTURE)
### Enhancements
* [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing
* [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel
* [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields
### Bug Fixes
* [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug
--- ---
## v3.2.8 (2022-08-08) ## v3.2.8 (2022-08-08)
+2 -2
View File
@@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from utilities import filters from utilities import filters
from utilities.forms import ( from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, 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.querysets import RestrictedQuerySet
from utilities.validators import validate_regex from utilities.validators import validate_regex
@@ -355,7 +355,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# JSON # JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON: elif self.type == CustomFieldTypeChoices.TYPE_JSON:
field = forms.JSONField(required=required, initial=initial) field = JSONField(required=required, initial=initial)
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+44
View File
@@ -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. * 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 { export function initSearch(): void {
for (const func of [initSearchBar]) { for (const func of [initSearchBar]) {
func(); func();
} }
initQuickSearch();
} }
+22 -4
View File
@@ -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 { main.layout {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
height: 18rem; height: 18rem;
} }
textarea#id_local_context_data,
textarea.markdown, textarea.markdown,
textarea#id_public_key, textarea.form-control[name='csv'] {
textarea.form-control[name='csv'],
textarea.form-control[name='data'] {
font-family: $font-family-monospace; font-family: $font-family-monospace;
} }
@@ -34,3 +34,11 @@ a[type='button'] {
.badge { .badge {
font-size: $font-size-xs; 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;
}
@@ -92,6 +92,10 @@ $input-focus-color: $input-color;
$input-placeholder-color: $gray-700; $input-placeholder-color: $gray-700;
$input-plaintext-color: $body-color; $input-plaintext-color: $body-color;
input {
color-scheme: dark;
}
$form-check-input-active-filter: brightness(90%); $form-check-input-active-filter: brightness(90%);
$form-check-input-bg: $input-bg; $form-check-input-bg: $input-bg;
$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25); $form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);
@@ -22,7 +22,6 @@ $theme-colors: (
'danger': $danger, 'danger': $danger,
'light': $light, 'light': $light,
'dark': $dark, 'dark': $dark,
// General-purpose palette // General-purpose palette
'blue': $blue-500, 'blue': $blue-500,
'indigo': $indigo-500, 'indigo': $indigo-500,
@@ -36,7 +35,7 @@ $theme-colors: (
'cyan': $cyan-500, 'cyan': $cyan-500,
'gray': $gray-500, 'gray': $gray-500,
'black': $black, 'black': $black,
'white': $white, 'white': $white
); );
$light: $gray-200; $light: $gray-200;
@@ -42,3 +42,9 @@ table td {
visibility: visible !important; visibility: visible !important;
} }
} }
// Hides the last child of an element
.hide-last-child :last-child {
visibility: hidden;
opacity: 0;
}
+71 -70
View File
@@ -4,81 +4,82 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="row mb-3 justify-content-between"> <div class="row mb-3 justify-content-between">
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls"> <div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm quicksearch hide-last-child">
<input <input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
type="text" hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
name="q" <button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
class="form-control" class="mdi mdi-close-circle"></i></button>
placeholder="Quick search"
hx-get="{{ request.full_path }}"
hx-target="#object_list"
hx-trigger="keyup changed delay:500ms"
/>
</div>
</div> </div>
<div class="col col-md-3 mb-0 d-flex noprint table-controls"> </div>
<div class="input-group input-group-sm justify-content-end"> <div class="col col-md-3 mb-0 d-flex noprint table-controls">
{% if request.user.is_authenticated %} <div class="input-group input-group-sm justify-content-end">
<button {% if request.user.is_authenticated %}
type="button" <button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal"
class="btn btn-sm btn-outline-dark" data-bs-target="#DeviceInterfaceTable_config" title="Configure Table">
data-bs-toggle="modal" <i class="mdi mdi-cog"></i> Configure Table
data-bs-target="#DeviceInterfaceTable_config" </button>
title="Configure Table"> {% endif %}
<i class="mdi mdi-cog"></i> Configure Table <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown"
</button> aria-expanded="false">
{% endif %} <i class="mdi mdi-eye"></i>
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> </button>
<i class="mdi mdi-eye"></i> <ul class="dropdown-menu">
</button> <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<ul class="dropdown-menu"> <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button> </ul>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button> </div>
</ul> </div>
</div> </div>
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div> </div>
</div> </div>
<form method="post"> <div class="noprint bulk-buttons">
{% csrf_token %} <div class="bulk-button-group">
{% if perms.dcim.change_interface %}
<button type="submit" name="_rename"
<div class="card"> formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
<div class="card-body" id="object_list"> class="btn btn-outline-warning btn-sm">
{% include 'htmx/table.html' %} <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
<button type="submit" name="_edit"
formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_disconnect"
formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_interface %}
<button type="submit" name="_delete"
formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_interface %}
<div class="bulk-button-group">
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
</a>
</div> </div>
</div> {% endif %}
</div>
<div class="noprint bulk-buttons"> </form>
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_interface %}
<div class="bulk-button-group">
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %} {% endblock %}
{% block modals %} {% block modals %}
+16
View File
@@ -10,6 +10,8 @@
<th>Name</th> <th>Name</th>
<th>Role</th> <th>Role</th>
<th>Priority</th> <th>Priority</th>
<th>Phone</th>
<th>Email</th>
<th></th> <th></th>
</tr> </tr>
{% for contact in contacts %} {% for contact in contacts %}
@@ -17,6 +19,20 @@
<td>{{ contact.contact|linkify }}</td> <td>{{ contact.contact|linkify }}</td>
<td>{{ contact.role|placeholder }}</td> <td>{{ contact.role|placeholder }}</td>
<td>{{ contact.get_priority_display|placeholder }}</td> <td>{{ contact.get_priority_display|placeholder }}</td>
<td>
{% if contact.contact.phone %}
<a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>
{% if contact.contact.email %}
<a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="text-end noprint"> <td class="text-end noprint">
{% if perms.tenancy.change_contactassignment %} {% if perms.tenancy.change_contactassignment %}
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit"> <a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">
+12 -22
View File
@@ -2,31 +2,21 @@
<div class="row mb-3 justify-content-between"> <div class="row mb-3 justify-content-between">
<div class="table-controls noprint col col-12 col-md-8 col-lg-4"> <div class="table-controls noprint col col-12 col-md-8 col-lg-4">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm quicksearch hide-last-child">
<input <input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
type="text" hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
name="q" <button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
class="form-control" class="mdi mdi-close-circle"></i></button>
placeholder="Quick search"
hx-get="{{ request.full_path }}"
hx-target="#object_list"
hx-trigger="keyup changed delay:500ms"
/>
</div> </div>
</div> </div>
<div class="table-controls noprint col col-md-3 mb-0"> <div class="table-controls noprint col col-md-3 mb-0">
{% if request.user.is_authenticated and table_modal %} {% if request.user.is_authenticated and table_modal %}
<div class="table-configure input-group input-group-sm"> <div class="table-configure input-group input-group-sm">
<button <button type="button" data-bs-toggle="modal" title="Configure Table" data-bs-target="#{{ table_modal }}"
type="button" class="btn btn-sm btn-outline-dark">
data-bs-toggle="modal" <i class="mdi mdi-cog"></i> Configure Table
title="Configure Table" </button>
data-bs-target="#{{ table_modal }}" </div>
class="btn btn-sm btn-outline-dark"
>
<i class="mdi mdi-cog"></i> Configure Table
</button>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
+1
View File
@@ -99,6 +99,7 @@ class JSONField(_JSONField):
if not self.help_text: if not self.help_text:
self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.' self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
self.widget.attrs['placeholder'] = '' self.widget.attrs['placeholder'] = ''
self.widget.attrs['class'] = 'font-monospace'
def prepare_value(self, value): def prepare_value(self, value):
if isinstance(value, InvalidJSONInput): if isinstance(value, InvalidJSONInput):
+1 -1
View File
@@ -136,7 +136,7 @@ class ImportForm(BootstrapMixin, forms.Form):
Generic form for creating an object from JSON/YAML data Generic form for creating an object from JSON/YAML data
""" """
data = forms.CharField( 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." help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
) )
format = forms.ChoiceField( format = forms.ChoiceField(
+1 -1
View File
@@ -93,7 +93,7 @@ class VirtualMachineFilterForm(
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')), ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
('Location', ('region_id', 'site_group_id', 'site_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')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
) )
+3
View File
@@ -35,3 +35,6 @@ tzdata==2022.1
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0
# Workaround for #9986
pytz==2022.1