Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2022-08-12 10:18:57 -04:00
commit e4bbebf87a
21 changed files with 203 additions and 103 deletions

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.

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)

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:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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();
} }

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;
} }

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

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 %}

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">

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>

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):

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(

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')),
) )

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