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

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

Removes the client-side interfaceTable.ts JavaScript (which hid/showed rows
via CSS without updating server queryset, causing incorrect pagination counts)
and replaces the toggle buttons with URL-based links that pass filter params
to the existing server-side InterfaceFilterSet. Adds a collapsible filter
panel to the generic object_children template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brian Tiemann 1 день назад
Родитель
Сommit
5f30d19aca

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

@@ -785,12 +785,9 @@ class DeviceInterfaceTable(InterfaceTable):
         )
         )
         row_attrs = {
         row_attrs = {
             'data-name': lambda record: record.name,
             'data-name': lambda record: record.name,
-            'data-enabled': lambda record: "enabled" if record.enabled else "disabled",
             'data-virtual': lambda record: "true" if record.is_virtual else "false",
             'data-virtual': lambda record: "true" if record.is_virtual else "false",
             'data-mark-connected': lambda record: "true" if record.mark_connected 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-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"
         }
         }
 
 
 
 

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


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


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

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

+ 0 - 1
netbox/project-static/src/tables/index.ts

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

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

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

+ 28 - 5
netbox/templates/dcim/device/inc/interface_table_controls.html

@@ -3,12 +3,35 @@
 
 
 {% block extra_table_controls %}
 {% block extra_table_controls %}
   <button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
   <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>
   </button>
   <ul class="dropdown-menu">
   <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>
   </ul>
 {% endblock extra_table_controls %}
 {% endblock extra_table_controls %}

+ 16 - 0
netbox/templates/generic/object_children.html

@@ -26,6 +26,22 @@ Context:
     {% block table_controls %}
     {% block table_controls %}
         {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
         {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
     {% endblock table_controls %}
     {% endblock table_controls %}
+    {% if filter_form %}
+        {% applied_filters model filter_form request.GET %}
+        <div class="mb-3 d-print-none">
+            <button class="btn btn-sm btn-outline-secondary" type="button"
+                    data-bs-toggle="collapse" data-bs-target="#filters-form"
+                    aria-expanded="{% if filter_form.changed_data %}true{% else %}false{% endif %}"
+                    aria-controls="filters-form">
+                <i class="mdi mdi-filter" aria-hidden="true"></i>
+                {% trans "Filters" %}
+                {% badge filter_form.changed_data|length bg_color="primary" %}
+            </button>
+        </div>
+        <div class="collapse {% if filter_form.changed_data %}show{% endif %}" id="filters-form">
+            {% include 'inc/filter_list.html' %}
+        </div>
+    {% endif %}
     <form method="post">
     <form method="post">
         {% csrf_token %}
         {% csrf_token %}
         <div class="card">
         <div class="card">

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