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

implement table filtering on generic object list

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

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


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


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


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


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


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


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


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


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


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


+ 5 - 0
netbox/project-static/netbox.scss

@@ -452,3 +452,8 @@ div.card-overlay {
     color: $secondary;
     color: $secondary;
   }
   }
 }
 }
+
+div.card > div.card-header > div.table-controls {
+  max-width: 25%;
+  width: 100%;
+}

+ 1 - 0
netbox/project-static/package.json

@@ -19,6 +19,7 @@
     "flatpickr": "4.6.3",
     "flatpickr": "4.6.3",
     "jquery": "3.5.1",
     "jquery": "3.5.1",
     "jquery-ui": "1.12.1",
     "jquery-ui": "1.12.1",
+    "just-debounce-it": "^1.4.0",
     "masonry-layout": "^4.2.2",
     "masonry-layout": "^4.2.2",
     "parcel-bundler": "1.12.3",
     "parcel-bundler": "1.12.3",
     "query-string": "^6.14.1",
     "query-string": "^6.14.1",

+ 60 - 10
netbox/project-static/src/search.ts

@@ -1,4 +1,5 @@
-import { getElements } from './util';
+import debounce from 'just-debounce-it';
+import { getElements, getRowValues, findFirstAdjacent } from './util';
 
 
 interface SearchFilterButton extends EventTarget {
 interface SearchFilterButton extends EventTarget {
   dataset: { searchValue: string };
   dataset: { searchValue: string };
@@ -42,20 +43,21 @@ function initSearchBar() {
  * Initialize Interface Table Filter Elements.
  * Initialize Interface Table Filter Elements.
  */
  */
 function initInterfaceFilter() {
 function initInterfaceFilter() {
-  for (const element of getElements<HTMLInputElement>('input.interface-filter')) {
+  for (const input of getElements<HTMLInputElement>('input.interface-filter')) {
+    const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
+    const rows = Array.from(
+      table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
+    ).filter(r => r !== null);
     /**
     /**
      * Filter on-page table by input text.
      * Filter on-page table by input text.
      */
      */
     function handleInput(event: Event) {
     function handleInput(event: Event) {
       const target = event.target as HTMLInputElement;
       const target = event.target as HTMLInputElement;
       // Create a regex pattern from the input search text to match against.
       // Create a regex pattern from the input search text to match against.
-      const filter = new RegExp(target.value);
+      const filter = new RegExp(target.value.toLowerCase().trim());
 
 
       // Each row represents an interface and its attributes.
       // Each row represents an interface and its attributes.
-      for (const row of getElements<HTMLTableRowElement>('table > tbody > tr')) {
-        // The data-name attribute's value contains the interface name.
-        const name = row.getAttribute('data-name');
-
+      for (const row of rows) {
         // Find the row's checkbox and deselect it, so that it is not accidentally included in form
         // Find the row's checkbox and deselect it, so that it is not accidentally included in form
         // submissions.
         // submissions.
         const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
         const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
@@ -63,8 +65,11 @@ function initInterfaceFilter() {
           checkBox.checked = false;
           checkBox.checked = false;
         }
         }
 
 
+        // The data-name attribute's value contains the interface name.
+        const name = row.getAttribute('data-name');
+
         if (typeof name === 'string') {
         if (typeof name === 'string') {
-          if (filter.test(name)) {
+          if (filter.test(name.toLowerCase().trim())) {
             // If this row matches the search pattern, but is already hidden, unhide it.
             // If this row matches the search pattern, but is already hidden, unhide it.
             if (row.classList.contains('d-none')) {
             if (row.classList.contains('d-none')) {
               row.classList.remove('d-none');
               row.classList.remove('d-none');
@@ -76,12 +81,57 @@ function initInterfaceFilter() {
         }
         }
       }
       }
     }
     }
-    element.addEventListener('keyup', handleInput);
+    input.addEventListener('keyup', debounce(handleInput, 300));
+  }
+}
+
+function initTableFilter() {
+  for (const input of getElements<HTMLInputElement>('input.object-filter')) {
+    // Find the first adjacent table element.
+    const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
+
+    // Build a valid array of <tr/> elements that are children of the adjacent table.
+    const rows = Array.from(
+      table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
+    ).filter(r => r !== null);
+
+    /**
+     * Filter table rows by matched input text.
+     * @param event
+     */
+    function handleInput(event: Event) {
+      const target = event.target as HTMLInputElement;
+
+      // Create a regex pattern from the input search text to match against.
+      const filter = new RegExp(target.value.toLowerCase().trim());
+
+      for (const row of rows) {
+        // Find the row's checkbox and deselect it, so that it is not accidentally included in form
+        // submissions.
+        const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
+        if (checkBox !== null) {
+          checkBox.checked = false;
+        }
+        // Iterate through each row's cell values
+        for (const value of getRowValues(row)) {
+          if (filter.test(value.toLowerCase())) {
+            // If this row matches the search pattern, but is already hidden, unhide it and stop
+            // iterating through the rest of the cells.
+            row.classList.remove('d-none');
+            break;
+          } else {
+            // If none of the cells in this row match the search pattern, hide the row.
+            row.classList.add('d-none');
+          }
+        }
+      }
+    }
+    input.addEventListener('keyup', debounce(handleInput, 300));
   }
   }
 }
 }
 
 
 export function initSearch() {
 export function initSearch() {
-  for (const func of [initSearchBar, initInterfaceFilter]) {
+  for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) {
     func();
     func();
   }
   }
 }
 }

+ 48 - 0
netbox/project-static/src/util.ts

@@ -5,6 +5,16 @@ type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
 type ReqData = URLSearchParams | Dict | undefined | unknown;
 type ReqData = URLSearchParams | Dict | undefined | unknown;
 type SelectedOption = { name: string; options: string[] };
 type SelectedOption = { name: string; options: string[] };
 
 
+// interface TableValue {
+//   row: {
+//     element: HTMLTableRowElement;
+//   };
+//   cell: {
+//     element: HTMLTableCellElement;
+//     value: string;
+//   };
+// }
+
 export function isApiError(data: Record<string, unknown>): data is APIError {
 export function isApiError(data: Record<string, unknown>): data is APIError {
   return 'error' in data && 'exception' in data;
   return 'error' in data && 'exception' in data;
 }
 }
@@ -202,3 +212,41 @@ export function toggleLoader(action: 'show' | 'hide') {
     }
     }
   }
   }
 }
 }
+
+/**
+ * Get the value of every cell in a table.
+ * @param table Table Element
+ */
+export function* getRowValues(table: HTMLTableRowElement): Generator<string> {
+  for (const element of table.querySelectorAll<HTMLTableCellElement>('td')) {
+    if (element !== null) {
+      if (isTruthy(element.innerText) && element.innerText !== '—') {
+        yield element.innerText.replaceAll(/[\n\r]/g, '').trim();
+      }
+    }
+  }
+}
+
+/**
+ * Recurse upward through an element's siblings until an element matching the query is found.
+ *
+ * @param base Base Element
+ * @param query CSS Query
+ */
+export function findFirstAdjacent<R extends HTMLElement, B extends Element = Element>(
+  base: B,
+  query: string,
+): Nullable<R> {
+  function match<P extends Element | null>(parent: P): Nullable<R> {
+    if (parent !== null && parent.parentElement !== null) {
+      for (const child of parent.parentElement.querySelectorAll<R>(query)) {
+        if (child !== null) {
+          return child;
+        }
+      }
+      return match(parent.parentElement.parentElement);
+    }
+    return null;
+  }
+  return match(base);
+}

+ 5 - 0
netbox/project-static/yarn.lock

@@ -4715,6 +4715,11 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     json-schema "0.2.3"
     verror "1.10.0"
     verror "1.10.0"
 
 
+just-debounce-it@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-1.4.0.tgz#02c8c95a1bdb70697e72e37fa64ca8689c10e78c"
+  integrity sha512-D6wp9toCJ77OAL8AvY+fgcNLlR9NC4HKnz6yx6r/IrOFcuDYdqk+P9asMg9nTLYT24Wpu1sT0lukDES6uvQvqA==
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"

+ 2 - 2
netbox/templates/dcim/device/interfaces.html

@@ -9,11 +9,11 @@
         <div class="card">
         <div class="card">
             <div class="card-header">
             <div class="card-header">
                 <h5 class="d-inline">Interfaces</h5>
                 <h5 class="d-inline">Interfaces</h5>
-                <div class="float-end col-md-2 noprint">
+                <div class="float-end col-md-2 noprint table-controls">
                     <div class="input-group input-group-sm">
                     <div class="input-group input-group-sm">
                         <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
                         <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
                         {% if request.user.is_authenticated %}
                         {% 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-cog"></i> Configure</button>
+                            <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 %}
                         {% endif %}
                     </div>
                     </div>
                 </div>
                 </div>

+ 58 - 56
netbox/templates/generic/object_list.html

@@ -7,7 +7,7 @@
 {% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
 {% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
 
 
 {% block controls %}
 {% block controls %}
-<div class="container mb-2 mx-0">
+<div class="controls mb-2 mx-0">
   <div class="d-flex flex-wrap justify-content-end">
   <div class="d-flex flex-wrap justify-content-end">
     {% block extra_controls %}{% endblock %}
     {% block extra_controls %}{% endblock %}
     {% if permissions.add and 'add' in action_buttons %}
     {% if permissions.add and 'add' in action_buttons %}
@@ -19,19 +19,6 @@
     {% if 'export' in action_buttons %}
     {% if 'export' in action_buttons %}
         {% export_button content_type %}
         {% export_button content_type %}
     {% endif %}
     {% endif %}
-    
-    <div class="d-flex flex-shrink-1">
-    {% if request.user.is_authenticated and table_config_form %}
-      <button
-        type="button"
-        class="btn btn-sm btn-outline-secondary m-1"
-        data-toggle="modal" data-target="#ObjectTable_config"
-        title="Configure table"
-      >
-        <i class="bi bi-sliders"></i>
-      </button>
-      {% endif %}
-    </div>
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -39,56 +26,71 @@
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
     <div class="col-9">
     <div class="col-9">
-        {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
-        {% if permissions.change or permissions.delete %}
-        <form method="post" class="form form-horizontal">
-            {% csrf_token %}
-            <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
-            {% if table.paginator.num_pages > 1 %}
-            <div id="select_all_box" class="hidden card noprint">
-                <div class="card-body">
-                    <div class="checkbox-inline">
-                        <label for="select_all">
-                            <input type="checkbox" id="select_all" name="_all" />
-                            Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
-                        </label>
+        <div class="card">
+            <div class="card-header">
+                <div class="float-end col-md-2 noprint table-controls">
+                    <div class="input-group input-group-sm">
+                        <input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
+                        {% if request.user.is_authenticated and table_config_form %}
+                            <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#ObjectTable_config" title="Configure Table">
+                                <i class="mdi mdi-table-eye"></i>
+                            </button>
+                        {% endif %}
+                    </div>
+                </div>
+            </div>
+            <div class="card-body">
+                {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
+                {% if permissions.change or permissions.delete %}
+                <form method="post" class="form form-horizontal">
+                    {% csrf_token %}
+                    <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
+                    {% if table.paginator.num_pages > 1 %}
+                    <div id="select_all_box" class="hidden card noprint">
+                        <div class="card-body">
+                            <div class="checkbox-inline">
+                                <label for="select_all">
+                                    <input type="checkbox" id="select_all" name="_all" />
+                                    Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> Matching Query
+                                </label>
+                            </div>
+                            <div class="float-end">
+                                {% if bulk_edit_url and permissions.change %}
+                                    <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
+                                        <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
+                                    </button>
+                                {% endif %}
+                                {% if bulk_delete_url and permissions.delete %}
+                                    <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
+                                        <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
+                                    </button>
+                                {% endif %}
+                            </div>
+                        </div>
                     </div>
                     </div>
-                    <div class="float-end">
+                    {% endif %}
+                    {% include table_template|default:'responsive_table.html' %}
+                    <div class="float-start noprint bulk-buttons">
+                        {% block bulk_buttons %}{% endblock %}
                         {% if bulk_edit_url and permissions.change %}
                         {% if bulk_edit_url and permissions.change %}
-                            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
-                                <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
-                            </button>
+                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
+                            <i class="bi bi-pencil-fill" aria-hidden="true"></i> Edit Selected
+                        </button>
                         {% endif %}
                         {% endif %}
                         {% if bulk_delete_url and permissions.delete %}
                         {% if bulk_delete_url and permissions.delete %}
-                            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
-                                <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
-                            </button>
+                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
+                            <i class="bi bi-trash-fill" aria-hidden="true"></i> Delete Selected
+                        </button>
                         {% endif %}
                         {% endif %}
                     </div>
                     </div>
-                </div>
-            </div>
-            {% endif %}
-            {% include table_template|default:'responsive_table.html' %}
-            <div class="float-start noprint bulk-buttons">
-                {% block bulk_buttons %}{% endblock %}
-                {% if bulk_edit_url and permissions.change %}
-                <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
-                    <i class="bi bi-pencil-fill" aria-hidden="true"></i> Edit Selected
-                </button>
-                {% endif %}
-                {% if bulk_delete_url and permissions.delete %}
-                <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
-                    <i class="bi bi-trash-fill" aria-hidden="true"></i> Delete Selected
-                </button>
+                </form>
+                {% else %}
+                    {% include table_template|default:'responsive_table.html' %}
                 {% endif %}
                 {% endif %}
+                {% endwith %}
+                {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
             </div>
             </div>
-        </form>
-        {% else %}
-            {% include table_template|default:'responsive_table.html' %}
-        {% endif %}
-        {% endwith %}
-        {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
-        <div class="clearfix"></div>
+        </div>
     </div>
     </div>
     {% if filter_form %}
     {% if filter_form %}
     <div class="col-3 noprint">
     <div class="col-3 noprint">

+ 2 - 2
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -9,11 +9,11 @@
         <div class="card my-3">
         <div class="card my-3">
             <div class="card-header">
             <div class="card-header">
                 <h5>Interfaces</h5>
                 <h5>Interfaces</h5>
-                <div class="float-end col-md-2 noprint">
+                <div class="float-end col-md-2 noprint table-controls">
                     <div class="input-group input-group-sm">
                     <div class="input-group input-group-sm">
                         <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
                         <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
                         {% if request.user.is_authenticated %}
                         {% if request.user.is_authenticated %}
-                            <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#VirtualMachineVMInterfaceTable_config" title="Configure Table"><i class="mdi mdi-cog"></i> Configure</button>
+                            <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#VirtualMachineVMInterfaceTable_config" title="Configure Table"><i class="mdi mdi-table-eye"></i></button>
                         {% endif %}
                         {% endif %}
                     </div>
                     </div>
                 </div>
                 </div>

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