Closes: #19336 - Replace JS interface table toggles with server-side URL filters (#22263)

This commit is contained in:
bctiemann 2026-05-26 08:38:10 -04:00 committed by GitHub
parent 2a19eb9901
commit 648d56010d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 37 additions and 270 deletions

View File

@ -789,8 +789,6 @@ class DeviceInterfaceTable(InterfaceTable):
'data-virtual': lambda record: "true" if record.is_virtual else "false",
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
'data-cable-status': lambda record: record.cable.status if record.cable else "",
'data-type': lambda record: record.type,
'data-connected': lambda record: "connected" if record.mark_connected or record.cable else "disconnected"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,6 @@ import { initMessages } from './messages';
import { initClipboard } from './clipboard';
import { initDateSelector } from './dateSelector';
import { initTableConfig } from './tableConfig';
import { initInterfaceTable } from './tables';
import { initSideNav } from './sidenav';
import { initDashboard } from './dashboard';
import { initRackElevation } from './racks';
@ -28,7 +27,6 @@ function initDocument(): void {
initButtons,
initClipboard,
initTableConfig,
initInterfaceTable,
initSideNav,
initDashboard,
initRackElevation,

View File

@ -1 +0,0 @@
export * from './interfaceTable';

View File

@ -1,251 +0,0 @@
import { getElements, replaceAll, findFirstAdjacent } from '../util';
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 provided in constructor
*/
private rows: NodeListOf<HTMLTableRowElement>;
constructor(button: HTMLButtonElement, rows: NodeListOf<HTMLTableRowElement>) {
this.button = button;
this.rows = rows;
}
/**
* Remove visibility of button state rows.
*/
private hideRows(): void {
for (const row of this.rows) {
row.classList.add('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 = replaceAll(this.button.innerText, 'Show', 'Hide');
} else if (this.buttonState === 'hide') {
this.button.innerText = replaceAll(this.button.innerHTML, 'Hide', 'Show');
}
}
/**
* 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();
}
/**
* When the button is clicked, toggle all controlled elements and hide rows based on
* buttonstate.
*/
public handleClick(event: Event): void {
const button = event.currentTarget as HTMLButtonElement;
if (button.isEqualNode(this.button)) {
this.toggle();
}
if (this.buttonState === 'hide') {
this.hideRows();
}
}
}
/**
* 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-expect-error null handling is performed in the constructor
private enabledButton: ButtonState;
/**
* Instance of ButtonState for the 'show/hide disabled rows' button.
*/
// @ts-expect-error null handling is performed in the constructor
private disabledButton: ButtonState;
/**
* Instance of ButtonState for the 'show/hide virtual rows' button.
*/
// @ts-expect-error null handling is performed in the constructor
private virtualButton: ButtonState;
/**
* Instance of ButtonState for the 'show/hide virtual rows' button.
*/
// @ts-expect-error null handling is performed in the constructor
private disconnectedButton: ButtonState;
/**
* All table rows in table
*/
private rows: NodeListOf<HTMLTableRowElement>;
constructor(table: HTMLTableElement) {
this.table = table;
this.rows = this.table.querySelectorAll('tr');
try {
const toggleEnabledButton = findFirstAdjacent<HTMLButtonElement>(
this.table,
'button.toggle-enabled',
);
const toggleDisabledButton = findFirstAdjacent<HTMLButtonElement>(
this.table,
'button.toggle-disabled',
);
const toggleVirtualButton = findFirstAdjacent<HTMLButtonElement>(
this.table,
'button.toggle-virtual',
);
const toggleDisconnectedButton = findFirstAdjacent<HTMLButtonElement>(
this.table,
'button.toggle-disconnected',
);
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);
}
if (toggleVirtualButton === null) {
throw new TableStateError("Table is missing a 'toggle-virtual' button.", table);
}
if (toggleDisconnectedButton === null) {
throw new TableStateError("Table is missing a 'toggle-disconnected' 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));
toggleVirtualButton.addEventListener('click', event => this.handleClick(event, this));
toggleDisconnectedButton.addEventListener('click', event => this.handleClick(event, this));
// Instantiate ButtonState for each button for state management.
this.enabledButton = new ButtonState(
toggleEnabledButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="enabled"]'),
);
this.disabledButton = new ButtonState(
toggleDisabledButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-enabled="disabled"]'),
);
this.virtualButton = new ButtonState(
toggleVirtualButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-type="virtual"]'),
);
this.disconnectedButton = new ButtonState(
toggleDisconnectedButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-connected="disconnected"]'),
);
} 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;
}
}
}
/**
* When toggle buttons are clicked, reapply visability all rows and
* pass the event to all button handlers
*
* @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 {
for (const row of this.rows) {
row.classList.remove('d-none');
}
instance.enabledButton.handleClick(event);
instance.disabledButton.handleClick(event);
instance.virtualButton.handleClick(event);
instance.disconnectedButton.handleClick(event);
}
}
/**
* Initialize table states.
*/
export function initInterfaceTable(): void {
for (const element of getElements<HTMLTableElement>('table')) {
new TableState(element);
}
}

View File

@ -3,12 +3,35 @@
{% block extra_table_controls %}
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i>
<i class="mdi mdi-eye" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">{% trans "Hide Enabled" %}</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">{% trans "Hide Disabled" %}</button>
<button type="button" class="dropdown-item toggle-virtual" data-state="show">{% trans "Hide Virtual" %}</button>
<button type="button" class="dropdown-item toggle-disconnected" data-state="show">{% trans "Hide Disconnected" %}</button>
{# enabled filter: True = show only enabled (hide disabled), False = show only disabled (hide enabled) #}
<li>
<a class="dropdown-item{% if request.GET.enabled == 'True' %} active{% endif %}"
href="{{ request.path }}{% if request.GET.enabled == 'True' %}{% querystring enabled=None %}{% else %}{% querystring enabled='True' %}{% endif %}">
{% if request.GET.enabled == 'True' %}<i class="mdi mdi-check" aria-hidden="true"></i> {% endif %}{% trans "Hide Disabled" %}
</a>
</li>
<li>
<a class="dropdown-item{% if request.GET.enabled == 'False' %} active{% endif %}"
href="{{ request.path }}{% if request.GET.enabled == 'False' %}{% querystring enabled=None %}{% else %}{% querystring enabled='False' %}{% endif %}">
{% if request.GET.enabled == 'False' %}<i class="mdi mdi-check" aria-hidden="true"></i> {% endif %}{% trans "Hide Enabled" %}
</a>
</li>
{# kind filter: physical = show only physical (hide virtual/wireless) #}
<li>
<a class="dropdown-item{% if request.GET.kind == 'physical' %} active{% endif %}"
href="{{ request.path }}{% if request.GET.kind == 'physical' %}{% querystring kind=None %}{% else %}{% querystring kind='physical' %}{% endif %}">
{% if request.GET.kind == 'physical' %}<i class="mdi mdi-check" aria-hidden="true"></i> {% endif %}{% trans "Hide Virtual" %}
</a>
</li>
{# connected filter: True = show only connected (hide disconnected) #}
<li>
<a class="dropdown-item{% if request.GET.connected == 'True' %} active{% endif %}"
href="{{ request.path }}{% if request.GET.connected == 'True' %}{% querystring connected=None %}{% else %}{% querystring connected='True' %}{% endif %}">
{% if request.GET.connected == 'True' %}<i class="mdi mdi-check" aria-hidden="true"></i> {% endif %}{% trans "Hide Disconnected" %}
</a>
</li>
</ul>
{% endblock extra_table_controls %}