Просмотр исходного кода

Closes #4247: Add option to show/hide enabled/disabled interfaces on device

checktheroads 4 лет назад
Родитель
Сommit
e004f872f9

+ 11 - 0
netbox/dcim/tables/devices.py

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-dark.css.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-light.css.map


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 37 - 8
netbox/project-static/netbox.scss

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

+ 2 - 0
netbox/project-static/src/netbox.ts

@@ -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 - 0
netbox/project-static/src/tables/index.ts

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

+ 295 - 0
netbox/project-static/src/tables/interfaceTable.ts

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

+ 18 - 4
netbox/templates/dcim/device/interfaces.html

@@ -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 - 0
netbox/templates/inc/table.html

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

Некоторые файлы не были показаны из-за большого количества измененных файлов