Merge branch 'develop' into feature
This commit is contained in:
commit
e4bbebf87a
|
|
@ -1,3 +1,3 @@
|
|||
## 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.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -355,7 +355,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:
|
||||
|
|
|
|||
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
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,81 +4,82 @@
|
|||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<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="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}"
|
||||
hx-target="#object_list"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
/>
|
||||
</div>
|
||||
<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="input-group input-group-sm quicksearch hide-last-child">
|
||||
<input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
||||
<button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
|
||||
class="mdi mdi-close-circle"></i></button>
|
||||
</div>
|
||||
<div class="col col-md-3 mb-0 d-flex noprint table-controls">
|
||||
<div class="input-group input-group-sm justify-content-end">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-dark"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#DeviceInterfaceTable_config"
|
||||
title="Configure Table">
|
||||
<i class="mdi mdi-cog"></i> Configure Table
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-eye"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
|
||||
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-3 mb-0 d-flex noprint table-controls">
|
||||
<div class="input-group input-group-sm justify-content-end">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal"
|
||||
data-bs-target="#DeviceInterfaceTable_config" title="Configure Table">
|
||||
<i class="mdi mdi-cog"></i> Configure Table
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="mdi mdi-eye"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
|
||||
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
|
||||
</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>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if perms.dcim.change_interface %}
|
||||
<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 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 class="noprint bulk-buttons">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Priority</th>
|
||||
<th>Phone</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for contact in contacts %}
|
||||
|
|
@ -17,6 +19,20 @@
|
|||
<td>{{ contact.contact|linkify }}</td>
|
||||
<td>{{ contact.role|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">
|
||||
{% 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">
|
||||
|
|
|
|||
|
|
@ -2,31 +2,21 @@
|
|||
|
||||
<div class="row mb-3 justify-content-between">
|
||||
<div class="table-controls noprint col col-12 col-md-8 col-lg-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="form-control"
|
||||
placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}"
|
||||
hx-target="#object_list"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
/>
|
||||
<div class="input-group input-group-sm quicksearch hide-last-child">
|
||||
<input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
||||
<button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
|
||||
class="mdi mdi-close-circle"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-controls noprint col col-md-3 mb-0">
|
||||
{% if request.user.is_authenticated and table_modal %}
|
||||
<div class="table-configure input-group input-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
title="Configure Table"
|
||||
data-bs-target="#{{ table_modal }}"
|
||||
class="btn btn-sm btn-outline-dark"
|
||||
>
|
||||
<i class="mdi mdi-cog"></i> Configure Table
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-configure input-group input-group-sm">
|
||||
<button type="button" data-bs-toggle="modal" title="Configure Table" data-bs-target="#{{ table_modal }}"
|
||||
class="btn btn-sm btn-outline-dark">
|
||||
<i class="mdi mdi-cog"></i> Configure Table
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -99,6 +99,7 @@ class JSONField(_JSONField):
|
|||
if not self.help_text:
|
||||
self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
|
||||
self.widget.attrs['placeholder'] = ''
|
||||
self.widget.attrs['class'] = 'font-monospace'
|
||||
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, InvalidJSONInput):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class VirtualMachineFilterForm(
|
|||
(None, ('q', 'tag')),
|
||||
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_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')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,3 +35,6 @@ tzdata==2022.1
|
|||
|
||||
# Workaround for #7401
|
||||
jsonschema==3.2.0
|
||||
|
||||
# Workaround for #9986
|
||||
pytz==2022.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue