mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 17:59:11 -06:00
Closes #4247: Add option to show/hide enabled/disabled interfaces on device
This commit is contained in:
parent
587335e3ba
commit
e004f872f9
@ -52,10 +52,20 @@ def get_cabletermination_row_class(record):
|
||||
return ''
|
||||
|
||||
|
||||
def get_interface_state_attribute(record):
|
||||
"""
|
||||
Get interface enabled state as string to attach to <tr/> DOM element.
|
||||
"""
|
||||
if record.enabled:
|
||||
return "enabled"
|
||||
else:
|
||||
return "disabled"
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
|
||||
class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(
|
||||
@ -528,6 +538,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
row_attrs = {
|
||||
'class': get_cabletermination_row_class,
|
||||
'data-name': lambda record: record.name,
|
||||
'data-enabled': get_interface_state_attribute,
|
||||
}
|
||||
|
||||
|
||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
File diff suppressed because one or more lines are too long
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.
@ -52,6 +52,26 @@
|
||||
transition: background-color, color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.mw-25 {
|
||||
max-width: 25% !important;
|
||||
}
|
||||
|
||||
.mw-33 {
|
||||
max-width: 33.33% !important;
|
||||
}
|
||||
|
||||
.mw-50 {
|
||||
max-width: 50% !important;
|
||||
}
|
||||
|
||||
.mw-66 {
|
||||
max-width: 66.66% !important;
|
||||
}
|
||||
|
||||
.mw-75 {
|
||||
max-width: 75% !important;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: $font-size-xs;
|
||||
line-height: $line-height-sm;
|
||||
@ -109,15 +129,18 @@ body {
|
||||
}
|
||||
}
|
||||
&[data-netbox-color-mode='dark'] {
|
||||
.btn.btn-primary,
|
||||
.progress-bar.bg-primary,
|
||||
.badge.bg-primary,
|
||||
.nav.nav-pills .nav-item .nav-link.active,
|
||||
.nav.nav-pills .nav-item .show > .nav-link {
|
||||
color: $black;
|
||||
& {
|
||||
.btn.btn-primary,
|
||||
.progress-bar.bg-primary,
|
||||
.badge.bg-primary,
|
||||
.nav.nav-pills .nav-item .nav-link.active,
|
||||
.nav.nav-pills .nav-item .show > .nav-link {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
.card table caption {
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
&[data-netbox-color-mode='dark'] {
|
||||
a:not(.btn) {
|
||||
color: $blue-300;
|
||||
}
|
||||
@ -726,4 +749,10 @@ div.card-overlay {
|
||||
div.card > div.card-header > div.table-controls {
|
||||
max-width: 25%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& .form-switch.form-check-inline {
|
||||
flex: 1 0 auto;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { initMessages } from './messages';
|
||||
import { initClipboard } from './clipboard';
|
||||
import { initDateSelector } from './dateSelector';
|
||||
import { initTableConfig } from './tableConfig';
|
||||
import { initInterfaceTable } from './tables';
|
||||
|
||||
function init() {
|
||||
for (const init of [
|
||||
@ -21,6 +22,7 @@ function init() {
|
||||
initButtons,
|
||||
initClipboard,
|
||||
initTableConfig,
|
||||
initInterfaceTable,
|
||||
]) {
|
||||
init();
|
||||
}
|
||||
|
1
netbox/project-static/src/tables/index.ts
Normal file
1
netbox/project-static/src/tables/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './interfaceTable';
|
295
netbox/project-static/src/tables/interfaceTable.ts
Normal file
295
netbox/project-static/src/tables/interfaceTable.ts
Normal file
@ -0,0 +1,295 @@
|
||||
import { getElements, findFirstAdjacent } from '../util';
|
||||
|
||||
type InterfaceState = 'enabled' | 'disabled';
|
||||
type ShowHide = 'show' | 'hide';
|
||||
|
||||
function isShowHide(value: unknown): value is ShowHide {
|
||||
return typeof value === 'string' && ['show', 'hide'].includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this error is thrown, it's an indication that we don't need to manage this table, because
|
||||
* it doesn't contain the required elements.
|
||||
*/
|
||||
class TableStateError extends Error {
|
||||
table: HTMLTableElement;
|
||||
constructor(message: string, table: HTMLTableElement) {
|
||||
super(message);
|
||||
this.table = table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the display text of a button element as well as the visibility of its corresponding rows.
|
||||
*/
|
||||
class ButtonState {
|
||||
/**
|
||||
* Underlying Button DOM Element
|
||||
*/
|
||||
public button: HTMLButtonElement;
|
||||
/**
|
||||
* Table rows with `data-enabled` set to `"enabled"`
|
||||
*/
|
||||
private enabledRows: NodeListOf<HTMLTableRowElement>;
|
||||
/**
|
||||
* Table rows with `data-enabled` set to `"disabled"`
|
||||
*/
|
||||
private disabledRows: NodeListOf<HTMLTableRowElement>;
|
||||
|
||||
constructor(button: HTMLButtonElement, table: HTMLTableElement) {
|
||||
this.button = button;
|
||||
this.enabledRows = table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="enabled"]');
|
||||
this.disabledRows = table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="disabled"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* This button's controlled type. For example, a button with the class `toggle-disabled` has
|
||||
* directive 'disabled' because it controls the visibility of rows with
|
||||
* `data-enabled="disabled"`. Likewise, `toggle-enabled` controls rows with
|
||||
* `data-enabled="enabled"`.
|
||||
*/
|
||||
private get directive(): InterfaceState {
|
||||
if (this.button.classList.contains('toggle-disabled')) {
|
||||
return 'disabled';
|
||||
} else if (this.button.classList.contains('toggle-enabled')) {
|
||||
return 'enabled';
|
||||
}
|
||||
// If this class has been instantiated but doesn't contain these classes, it's probably because
|
||||
// the classes are missing in the HTML template.
|
||||
console.warn(this.button);
|
||||
throw new Error('Toggle button does not contain expected class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of rows with `data-enabled="enabled"`.
|
||||
*/
|
||||
private toggleEnabledRows(): void {
|
||||
for (const row of this.enabledRows) {
|
||||
row.classList.toggle('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of rows with `data-enabled="disabled"`.
|
||||
*/
|
||||
private toggleDisabledRows(): void {
|
||||
for (const row of this.disabledRows) {
|
||||
row.classList.toggle('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the DOM element's `data-state` attribute.
|
||||
*/
|
||||
public set buttonState(state: Nullable<ShowHide>) {
|
||||
if (isShowHide(state)) {
|
||||
this.button.setAttribute('data-state', state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOM element's `data-state` attribute.
|
||||
*/
|
||||
public get buttonState(): Nullable<ShowHide> {
|
||||
const state = this.button.getAttribute('data-state');
|
||||
if (isShowHide(state)) {
|
||||
return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the DOM element's display text to reflect the action opposite the current state. For
|
||||
* example, if the current state is to hide enabled interfaces, the DOM text should say
|
||||
* "Show Enabled Interfaces".
|
||||
*/
|
||||
private toggleButton(): void {
|
||||
if (this.buttonState === 'show') {
|
||||
this.button.innerText = this.button.innerText.replaceAll('Show', 'Hide');
|
||||
} else if (this.buttonState === 'hide') {
|
||||
this.button.innerText = this.button.innerText.replaceAll('Hide', 'Show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility for the rows this element controls.
|
||||
*/
|
||||
private toggleRows(): void {
|
||||
if (this.directive === 'enabled') {
|
||||
this.toggleEnabledRows();
|
||||
} else if (this.directive === 'disabled') {
|
||||
this.toggleDisabledRows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the DOM element's `data-state` attribute.
|
||||
*/
|
||||
private toggleState(): void {
|
||||
if (this.buttonState === 'show') {
|
||||
this.buttonState = 'hide';
|
||||
} else if (this.buttonState === 'hide') {
|
||||
this.buttonState = 'show';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle all controlled elements.
|
||||
*/
|
||||
private toggle(): void {
|
||||
this.toggleState();
|
||||
this.toggleButton();
|
||||
this.toggleRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the button is clicked, toggle all controlled elements.
|
||||
*/
|
||||
public handleClick(event: Event): void {
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
if (button.isEqualNode(this.button)) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the state of a table and its elements.
|
||||
*/
|
||||
class TableState {
|
||||
/**
|
||||
* Underlying DOM Table Element.
|
||||
*/
|
||||
|
||||
private table: HTMLTableElement;
|
||||
/**
|
||||
* Instance of ButtonState for the 'show/hide enabled rows' button.
|
||||
*
|
||||
* TS Error is expected because null handling is performed in the constructor.
|
||||
*/
|
||||
// @ts-expect-error
|
||||
private enabledButton: ButtonState;
|
||||
|
||||
/**
|
||||
* Instance of ButtonState for the 'show/hide disabled rows' button.
|
||||
*
|
||||
* TS Error is expected because null handling is performed in the constructor.
|
||||
*/
|
||||
// @ts-expect-error
|
||||
private disabledButton: ButtonState;
|
||||
|
||||
/**
|
||||
* Underlying DOM Table Caption Element.
|
||||
*/
|
||||
private caption: Nullable<HTMLTableCaptionElement> = null;
|
||||
|
||||
constructor(table: HTMLTableElement) {
|
||||
this.table = table;
|
||||
|
||||
try {
|
||||
const toggleEnabledButton = findFirstAdjacent<HTMLButtonElement>(
|
||||
this.table,
|
||||
'button.toggle-enabled',
|
||||
);
|
||||
const toggleDisabledButton = findFirstAdjacent<HTMLButtonElement>(
|
||||
this.table,
|
||||
'button.toggle-disabled',
|
||||
);
|
||||
|
||||
const caption = this.table.querySelector('caption');
|
||||
this.caption = caption;
|
||||
|
||||
if (toggleEnabledButton === null) {
|
||||
throw new TableStateError("Table is missing a 'toggle-enabled' button.", table);
|
||||
}
|
||||
|
||||
if (toggleDisabledButton === null) {
|
||||
throw new TableStateError("Table is missing a 'toggle-disabled' button.", table);
|
||||
}
|
||||
|
||||
// Attach event listeners to the buttons elements.
|
||||
toggleEnabledButton.addEventListener('click', event => this.handleClick(event, this));
|
||||
toggleDisabledButton.addEventListener('click', event => this.handleClick(event, this));
|
||||
|
||||
// Instantiate ButtonState for each button for state management.
|
||||
this.enabledButton = new ButtonState(toggleEnabledButton, this.table);
|
||||
this.disabledButton = new ButtonState(toggleDisabledButton, this.table);
|
||||
} catch (err) {
|
||||
if (err instanceof TableStateError) {
|
||||
// This class is useless for tables that don't have toggle buttons.
|
||||
console.debug('Table does not contain enable/disable toggle buttons');
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table caption's text.
|
||||
*/
|
||||
private get captionText(): string {
|
||||
if (this.caption !== null) {
|
||||
return this.caption.innerText;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table caption's text.
|
||||
*/
|
||||
private set captionText(value: string) {
|
||||
if (this.caption !== null) {
|
||||
this.caption.innerText = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the table caption's text based on the state of each toggle button.
|
||||
*/
|
||||
private toggleCaption(): void {
|
||||
const showEnabled = this.enabledButton.buttonState === 'show';
|
||||
const showDisabled = this.disabledButton.buttonState === 'show';
|
||||
|
||||
if (showEnabled && !showDisabled) {
|
||||
this.captionText = 'Showing Enabled Interfaces';
|
||||
} else if (showEnabled && showDisabled) {
|
||||
this.captionText = 'Showing Enabled & Disabled Interfaces';
|
||||
} else if (!showEnabled && showDisabled) {
|
||||
this.captionText = 'Showing Disabled Interfaces';
|
||||
} else if (!showEnabled && !showDisabled) {
|
||||
this.captionText = 'Hiding Enabled & Disabled Interfaces';
|
||||
} else {
|
||||
this.captionText = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When toggle buttons are clicked, pass the event to the relevant button's handler and update
|
||||
* this instance's state.
|
||||
*
|
||||
* @param event onClick event for toggle buttons.
|
||||
* @param instance Instance of TableState (`this` cannot be used since that's context-specific).
|
||||
*/
|
||||
public handleClick(event: Event, instance: TableState): void {
|
||||
const button = event.currentTarget as HTMLButtonElement;
|
||||
const enabled = button.isEqualNode(instance.enabledButton.button);
|
||||
const disabled = button.isEqualNode(instance.disabledButton.button);
|
||||
|
||||
if (enabled) {
|
||||
instance.enabledButton.handleClick(event);
|
||||
} else if (disabled) {
|
||||
instance.disabledButton.handleClick(event);
|
||||
}
|
||||
instance.toggleCaption();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize table states.
|
||||
*/
|
||||
export function initInterfaceTable() {
|
||||
for (const element of getElements<HTMLTableElement>('table')) {
|
||||
new TableState(element);
|
||||
}
|
||||
}
|
@ -9,12 +9,26 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="d-inline">Interfaces</h5>
|
||||
<div class="float-end col-md-2 noprint table-controls">
|
||||
<div class="float-end col-md-4 noprint table-controls mw-33">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
|
||||
{% if request.user.is_authenticated %}
|
||||
<button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceInterfaceTable_config" title="Configure Table"><i class="mdi mdi-table-eye"></i></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-table-cog"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if request.user.is_authenticated %}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#DeviceInterfaceTable_config"
|
||||
title="Configure Table">
|
||||
Configure Table
|
||||
</button>
|
||||
{% endif %}
|
||||
<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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% load django_tables2 %}
|
||||
|
||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||
<caption class="text-center small mt-3"></caption>
|
||||
{% if table.show_header %}
|
||||
<thead>
|
||||
<tr>
|
||||
|
Loading…
Reference in New Issue
Block a user