mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 04:56:29 -06:00
implement table filtering on generic object list
This commit is contained in:
parent
d171e781d2
commit
acca69a8a9
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
2
netbox/project-static/dist/netbox.css.map
vendored
2
netbox/project-static/dist/netbox.css.map
vendored
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.
@ -452,3 +452,8 @@ div.card-overlay {
|
|||||||
color: $secondary;
|
color: $secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.card > div.card-header > div.table-controls {
|
||||||
|
max-width: 25%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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')) {
|
for (const row of rows) {
|
||||||
// The data-name attribute's value contains the interface name.
|
|
||||||
const name = row.getAttribute('data-name');
|
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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" %}
|
<div class="card">
|
||||||
{% if permissions.change or permissions.delete %}
|
<div class="card-header">
|
||||||
<form method="post" class="form form-horizontal">
|
<div class="float-end col-md-2 noprint table-controls">
|
||||||
{% csrf_token %}
|
<div class="input-group input-group-sm">
|
||||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
<input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
|
||||||
{% if table.paginator.num_pages > 1 %}
|
{% if request.user.is_authenticated and table_config_form %}
|
||||||
<div id="select_all_box" class="hidden card noprint">
|
<button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#ObjectTable_config" title="Configure Table">
|
||||||
<div class="card-body">
|
<i class="mdi mdi-table-eye"></i>
|
||||||
<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>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="card-body">
|
||||||
{% include table_template|default:'responsive_table.html' %}
|
{% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
|
||||||
<div class="float-start noprint bulk-buttons">
|
{% if permissions.change or permissions.delete %}
|
||||||
{% block bulk_buttons %}{% endblock %}
|
<form method="post" class="form form-horizontal">
|
||||||
{% if bulk_edit_url and permissions.change %}
|
{% csrf_token %}
|
||||||
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
|
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||||
<i class="bi bi-pencil-fill" aria-hidden="true"></i> Edit Selected
|
{% if table.paginator.num_pages > 1 %}
|
||||||
</button>
|
<div id="select_all_box" class="hidden card noprint">
|
||||||
{% endif %}
|
<div class="card-body">
|
||||||
{% if bulk_delete_url and permissions.delete %}
|
<div class="checkbox-inline">
|
||||||
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
|
<label for="select_all">
|
||||||
<i class="bi bi-trash-fill" aria-hidden="true"></i> Delete Selected
|
<input type="checkbox" id="select_all" name="_all" />
|
||||||
</button>
|
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>
|
||||||
|
{% 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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</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>
|
</div>
|
||||||
{% 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>
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
<div class="col-3 noprint">
|
<div class="col-3 noprint">
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user