diff --git a/.eslintrc.json b/.eslintrc.json
index 56f4296d..a313dfa2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -28,7 +28,7 @@
"class-methods-use-this": "off",
"eqeqeq": "warn",
"radix": "warn",
- "max-classes-per-file": ["error", 2]
+ "max-classes-per-file": "warn"
},
"globals": {
"API_URLS": true,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6c51b46..335cafa4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ ml).
## master
## testing
+- [added] #273 Allow search/filter lots on lots management component.
## [2.1.1] - 2022-05-11
Hot fix release.
diff --git a/ereuse_devicehub/static/js/main.js b/ereuse_devicehub/static/js/main.js
index 996d8b23..474658d5 100644
--- a/ereuse_devicehub/static/js/main.js
+++ b/ereuse_devicehub/static/js/main.js
@@ -1,353 +1,357 @@
-/**
-* Template Name: NiceAdmin - v2.2.0
-* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
-* Author: BootstrapMade.com
-* License: https://bootstrapmade.com/license/
-*/
-(function () {
- "use strict";
-
- /**
- * Easy selector helper function
- */
- const select = (el, all = false) => {
- el = el.trim()
- if (all) {
- return [...document.querySelectorAll(el)]
- }
- return document.querySelector(el)
-
- }
-
- /**
- * Easy event listener function
- */
- const on = (type, el, listener, all = false) => {
- if (all) {
- select(el, all).forEach(e => e.addEventListener(type, listener))
- } else {
- select(el, all).addEventListener(type, listener)
- }
- }
-
- /**
- * Easy on scroll event listener
- */
- const onscroll = (el, listener) => {
- el.addEventListener("scroll", listener)
- }
-
- /**
- * Sidebar toggle
- */
- if (select(".toggle-sidebar-btn")) {
- on("click", ".toggle-sidebar-btn", (e) => {
- select("body").classList.toggle("toggle-sidebar")
- })
- }
-
- /**
- * Search bar toggle
- */
- if (select(".search-bar-toggle")) {
- on("click", ".search-bar-toggle", (e) => {
- select(".search-bar").classList.toggle("search-bar-show")
- })
- }
-
- /**
- * Navbar links active state on scroll
- */
- const navbarlinks = select("#navbar .scrollto", true)
- const navbarlinksActive = () => {
- const position = window.scrollY + 200
- navbarlinks.forEach(navbarlink => {
- if (!navbarlink.hash) return
- const section = select(navbarlink.hash)
- if (!section) return
- if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
- navbarlink.classList.add("active")
- } else {
- navbarlink.classList.remove("active")
- }
- })
- }
- window.addEventListener("load", navbarlinksActive)
- onscroll(document, navbarlinksActive)
-
- /**
- * Toggle .header-scrolled class to #header when page is scrolled
- */
- const selectHeader = select("#header")
- if (selectHeader) {
- const headerScrolled = () => {
- if (window.scrollY > 100) {
- selectHeader.classList.add("header-scrolled")
- } else {
- selectHeader.classList.remove("header-scrolled")
- }
- }
- window.addEventListener("load", headerScrolled)
- onscroll(document, headerScrolled)
- }
-
- /**
- * Back to top button
- */
- const backtotop = select(".back-to-top")
- if (backtotop) {
- const toggleBacktotop = () => {
- if (window.scrollY > 100) {
- backtotop.classList.add("active")
- } else {
- backtotop.classList.remove("active")
- }
- }
- window.addEventListener("load", toggleBacktotop)
- onscroll(document, toggleBacktotop)
- }
-
- /**
- * Initiate tooltips
- */
- const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
- const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl))
-
- /**
- * Initiate quill editors
- */
- if (select(".quill-editor-default")) {
- new Quill(".quill-editor-default", {
- theme: "snow"
- });
- }
-
- if (select(".quill-editor-bubble")) {
- new Quill(".quill-editor-bubble", {
- theme: "bubble"
- });
- }
-
- if (select(".quill-editor-full")) {
- new Quill(".quill-editor-full", {
- modules: {
- toolbar: [
- [{
- font: []
- }, {
- size: []
- }],
- ["bold", "italic", "underline", "strike"],
- [{
- color: []
- },
- {
- background: []
- }
- ],
- [{
- script: "super"
- },
- {
- script: "sub"
- }
- ],
- [{
- list: "ordered"
- },
- {
- list: "bullet"
- },
- {
- indent: "-1"
- },
- {
- indent: "+1"
- }
- ],
- ["direction", {
- align: []
- }],
- ["link", "image", "video"],
- ["clean"]
- ]
- },
- theme: "snow"
- });
- }
-
- /**
- * Initiate Bootstrap validation check
- */
- const needsValidation = document.querySelectorAll(".needs-validation")
-
- Array.prototype.slice.call(needsValidation)
- .forEach((form) => {
- form.addEventListener("submit", (event) => {
- if (!form.checkValidity()) {
- event.preventDefault()
- event.stopPropagation()
- }
-
- form.classList.add("was-validated")
- }, false)
- })
-
- /**
- * Initiate Datatables
- */
- const datatables = select(".datatable", true)
- datatables.forEach(datatable => {
- new simpleDatatables.DataTable(datatable);
- })
-
- /**
- * Autoresize echart charts
- */
- const mainContainer = select("#main");
- if (mainContainer) {
- setTimeout(() => {
- new ResizeObserver(() => {
- select(".echart", true).forEach(getEchart => {
- echarts.getInstanceByDom(getEchart).resize();
- })
- }).observe(mainContainer);
- }, 200);
- }
-
- /**
- * Avoid hide dropdown when user clicked inside
- */
- document.getElementById("dropDownLotsSelector").addEventListener("click", event => {
- event.stopPropagation();
- })
-
- /**
- * Search form functionality
- */
- window.addEventListener("DOMContentLoaded", () => {
- const searchForm = document.getElementById("SearchForm")
- const inputSearch = document.querySelector("#SearchForm > input")
- const doSearch = true
-
- searchForm.addEventListener("submit", (event) => {
- event.preventDefault();
- })
-
- let timeoutHandler = setTimeout(() => { }, 1)
- const dropdownList = document.getElementById("dropdown-search-list")
- const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
-
-
- inputSearch.addEventListener("input", (e) => {
- clearTimeout(timeoutHandler)
- const searchText = e.target.value
- if (searchText == "") {
- document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
- return
- }
-
- let resultCount = 0;
- function searchCompleted() {
- resultCount++;
- setTimeout(() => {
- if (resultCount == 2 && document.getElementById("dropdown-search-list").children.length == 2) {
- document.getElementById("dropdown-search-list").innerHTML = `
-
-
- Nothing found
- `
- }
- }, 100)
- }
-
- timeoutHandler = setTimeout(async () => {
- dropdownList.innerHTML = `
-
-
-
- Loading...
-
-
-
-
-
- Loading...
-
- `;
-
-
- try {
- Api.search_device(searchText.toUpperCase()).then(devices => {
- dropdownList.querySelector("#deviceSearchLoader").style = "display: none"
-
- for (let i = 0; i < devices.length; i++) {
- const device = devices[i];
-
- // See: ereuse_devicehub/resources/device/models.py
- const verboseName = `${device.type} ${device.manufacturer} ${device.model}`
-
- const templateString = `
-
-
-
- ${verboseName}
- ${device.devicehubID}
-
- `;
- dropdownList.innerHTML += templateString
- if (i == 4) { // Limit to 4 resullts
- break;
- }
- }
-
- searchCompleted();
- })
- } catch (error) {
- dropdownList.innerHTML += `
-
-
-
- Error searching devices
-
- `;
- console.log(error);
- }
-
- try {
- Api.get_lots().then(lots => {
- dropdownList.querySelector("#lotSearchLoader").style = "display: none"
- for (let i = 0; i < lots.length; i++) {
- const lot = lots[i];
- if (lot.name.toUpperCase().includes(searchText.toUpperCase())) {
- const templateString = `
-
-
-
- ${lot.name}
-
- `;
- dropdownList.innerHTML += templateString
- if (i == 4) { // Limit to 4 resullts
- break;
- }
- }
- }
- searchCompleted();
- })
-
- } catch (error) {
- dropdownList.innerHTML += `
-
-
-
- Error searching lots
-
- `;
- console.log(error);
- }
- }, 1000)
- })
- })
-
-})();
+/**
+* Template Name: NiceAdmin - v2.2.0
+* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
+* Author: BootstrapMade.com
+* License: https://bootstrapmade.com/license/
+*/
+(function () {
+ "use strict";
+
+ /**
+ * Easy selector helper function
+ */
+ const select = (el, all = false) => {
+ el = el.trim()
+ if (all) {
+ return [...document.querySelectorAll(el)]
+ }
+ return document.querySelector(el)
+
+ }
+
+ /**
+ * Easy event listener function
+ */
+ const on = (type, el, listener, all = false) => {
+ if (all) {
+ select(el, all).forEach(e => e.addEventListener(type, listener))
+ } else {
+ select(el, all).addEventListener(type, listener)
+ }
+ }
+
+ /**
+ * Easy on scroll event listener
+ */
+ const onscroll = (el, listener) => {
+ el.addEventListener("scroll", listener)
+ }
+
+ /**
+ * Sidebar toggle
+ */
+ if (select(".toggle-sidebar-btn")) {
+ on("click", ".toggle-sidebar-btn", (e) => {
+ select("body").classList.toggle("toggle-sidebar")
+ })
+ }
+
+ /**
+ * Search bar toggle
+ */
+ if (select(".search-bar-toggle")) {
+ on("click", ".search-bar-toggle", (e) => {
+ select(".search-bar").classList.toggle("search-bar-show")
+ })
+ }
+
+ /**
+ * Navbar links active state on scroll
+ */
+ const navbarlinks = select("#navbar .scrollto", true)
+ const navbarlinksActive = () => {
+ const position = window.scrollY + 200
+ navbarlinks.forEach(navbarlink => {
+ if (!navbarlink.hash) return
+ const section = select(navbarlink.hash)
+ if (!section) return
+ if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
+ navbarlink.classList.add("active")
+ } else {
+ navbarlink.classList.remove("active")
+ }
+ })
+ }
+ window.addEventListener("load", navbarlinksActive)
+ onscroll(document, navbarlinksActive)
+
+ /**
+ * Toggle .header-scrolled class to #header when page is scrolled
+ */
+ const selectHeader = select("#header")
+ if (selectHeader) {
+ const headerScrolled = () => {
+ if (window.scrollY > 100) {
+ selectHeader.classList.add("header-scrolled")
+ } else {
+ selectHeader.classList.remove("header-scrolled")
+ }
+ }
+ window.addEventListener("load", headerScrolled)
+ onscroll(document, headerScrolled)
+ }
+
+ /**
+ * Back to top button
+ */
+ const backtotop = select(".back-to-top")
+ if (backtotop) {
+ const toggleBacktotop = () => {
+ if (window.scrollY > 100) {
+ backtotop.classList.add("active")
+ } else {
+ backtotop.classList.remove("active")
+ }
+ }
+ window.addEventListener("load", toggleBacktotop)
+ onscroll(document, toggleBacktotop)
+ }
+
+ /**
+ * Initiate tooltips
+ */
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
+ const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl))
+
+ /**
+ * Initiate quill editors
+ */
+ if (select(".quill-editor-default")) {
+ new Quill(".quill-editor-default", {
+ theme: "snow"
+ });
+ }
+
+ if (select(".quill-editor-bubble")) {
+ new Quill(".quill-editor-bubble", {
+ theme: "bubble"
+ });
+ }
+
+ if (select(".quill-editor-full")) {
+ new Quill(".quill-editor-full", {
+ modules: {
+ toolbar: [
+ [{
+ font: []
+ }, {
+ size: []
+ }],
+ ["bold", "italic", "underline", "strike"],
+ [{
+ color: []
+ },
+ {
+ background: []
+ }
+ ],
+ [{
+ script: "super"
+ },
+ {
+ script: "sub"
+ }
+ ],
+ [{
+ list: "ordered"
+ },
+ {
+ list: "bullet"
+ },
+ {
+ indent: "-1"
+ },
+ {
+ indent: "+1"
+ }
+ ],
+ ["direction", {
+ align: []
+ }],
+ ["link", "image", "video"],
+ ["clean"]
+ ]
+ },
+ theme: "snow"
+ });
+ }
+
+ /**
+ * Initiate Bootstrap validation check
+ */
+ const needsValidation = document.querySelectorAll(".needs-validation")
+
+ Array.prototype.slice.call(needsValidation)
+ .forEach((form) => {
+ form.addEventListener("submit", (event) => {
+ if (!form.checkValidity()) {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+
+ form.classList.add("was-validated")
+ }, false)
+ })
+
+ /**
+ * Initiate Datatables
+ */
+ const datatables = select(".datatable", true)
+ datatables.forEach(datatable => {
+ new simpleDatatables.DataTable(datatable);
+ })
+
+ /**
+ * Autoresize echart charts
+ */
+ const mainContainer = select("#main");
+ if (mainContainer) {
+ setTimeout(() => {
+ new ResizeObserver(() => {
+ select(".echart", true).forEach(getEchart => {
+ echarts.getInstanceByDom(getEchart).resize();
+ })
+ }).observe(mainContainer);
+ }, 200);
+ }
+
+ /**
+ * Avoid hide dropdown when user clicked inside
+ */
+ const dropdownLotSelector = document.getElementById("dropDownLotsSelector")
+ if (dropdownLotSelector != null) { // If exists selector it will set click event
+ dropdownLotSelector.addEventListener("click", event => {
+ event.stopPropagation();
+ })
+ }
+
+
+ /**
+ * Search form functionality
+ */
+ window.addEventListener("DOMContentLoaded", () => {
+ const searchForm = document.getElementById("SearchForm")
+ const inputSearch = document.querySelector("#SearchForm > input")
+ const doSearch = true
+
+ searchForm.addEventListener("submit", (event) => {
+ event.preventDefault();
+ })
+
+ let timeoutHandler = setTimeout(() => { }, 1)
+ const dropdownList = document.getElementById("dropdown-search-list")
+ const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
+
+
+ inputSearch.addEventListener("input", (e) => {
+ clearTimeout(timeoutHandler)
+ const searchText = e.target.value
+ if (searchText == "") {
+ document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
+ return
+ }
+
+ let resultCount = 0;
+ function searchCompleted() {
+ resultCount++;
+ setTimeout(() => {
+ if (resultCount == 2 && document.getElementById("dropdown-search-list").children.length == 2) {
+ document.getElementById("dropdown-search-list").innerHTML = `
+
+
+ Nothing found
+ `
+ }
+ }, 100)
+ }
+
+ timeoutHandler = setTimeout(async () => {
+ dropdownList.innerHTML = `
+
+
+
+ Loading...
+
+
+
+
+
+ Loading...
+
+ `;
+
+
+ try {
+ Api.search_device(searchText.toUpperCase()).then(devices => {
+ dropdownList.querySelector("#deviceSearchLoader").style = "display: none"
+
+ for (let i = 0; i < devices.length; i++) {
+ const device = devices[i];
+
+ // See: ereuse_devicehub/resources/device/models.py
+ const verboseName = `${device.type} ${device.manufacturer} ${device.model}`
+
+ const templateString = `
+
+
+
+ ${verboseName}
+ ${device.devicehubID}
+
+ `;
+ dropdownList.innerHTML += templateString
+ if (i == 4) { // Limit to 4 resullts
+ break;
+ }
+ }
+
+ searchCompleted();
+ })
+ } catch (error) {
+ dropdownList.innerHTML += `
+
+
+
+ Error searching devices
+
+ `;
+ console.log(error);
+ }
+
+ try {
+ Api.get_lots().then(lots => {
+ dropdownList.querySelector("#lotSearchLoader").style = "display: none"
+ for (let i = 0; i < lots.length; i++) {
+ const lot = lots[i];
+ if (lot.name.toUpperCase().includes(searchText.toUpperCase())) {
+ const templateString = `
+
+
+
+ ${lot.name}
+
+ `;
+ dropdownList.innerHTML += templateString
+ if (i == 4) { // Limit to 4 resullts
+ break;
+ }
+ }
+ }
+ searchCompleted();
+ })
+
+ } catch (error) {
+ dropdownList.innerHTML += `
+
+
+
+ Error searching lots
+
+ `;
+ console.log(error);
+ }
+ }, 1000)
+ })
+ })
+
+})();
diff --git a/ereuse_devicehub/static/js/main_inventory.build.js b/ereuse_devicehub/static/js/main_inventory.build.js
index 48ab00a7..34dd7bfa 100644
--- a/ereuse_devicehub/static/js/main_inventory.build.js
+++ b/ereuse_devicehub/static/js/main_inventory.build.js
@@ -1,5 +1,7 @@
"use strict";
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError("attempted to " + action + " private static field before its declaration"); } }
@@ -328,11 +330,59 @@ function export_file(type_file) {
$("#exportAlertModal").click();
}
}
+
+class lotsSearcher {
+ static enable() {
+ if (this.lotsSearchElement) this.lotsSearchElement.disabled = false;
+ }
+
+ static disable() {
+ if (this.lotsSearchElement) this.lotsSearchElement.disabled = true;
+ }
+ /**
+ * do search when lot change in the search input
+ */
+
+
+ static doSearch(inputSearch) {
+ const lots = this.getListLots();
+
+ for (let i = 0; i < lots.length; i++) {
+ if (lot.innerText.toLowerCase().includes(inputSearch.toLowerCase())) {
+ lot.parentElement.style.display = "";
+ } else {
+ lot.parentElement.style.display = "none";
+ }
+ }
+ }
+
+}
+
+_defineProperty(lotsSearcher, "lots", []);
+
+_defineProperty(lotsSearcher, "lotsSearchElement", null);
+
+_defineProperty(lotsSearcher, "getListLots", () => {
+ let lotsList = document.getElementById("LotsSelector");
+
+ if (lotsList) {
+ // Apply filter to get only labels
+ return Array.from(lotsList.children).filter(item => item.querySelector("label"));
+ }
+
+ return [];
+});
+
+document.addEventListener("DOMContentLoaded", () => {
+ lotsSearcher.lotsSearchElement = document.getElementById("lots-search");
+ lotsSearcher.lotsSearchElement.addEventListener("input", e => {
+ lotsSearcher.doSearch(e.target.value);
+ });
+});
/**
* Reactive lots button
*/
-
async function processSelectedDevices() {
class Actions {
constructor() {
@@ -584,6 +634,7 @@ async function processSelectedDevices() {
document.getElementById("ApplyDeviceLots").classList.add("disabled");
try {
+ lotsSearcher.disable();
listHTML.html("");
const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots();
@@ -614,6 +665,7 @@ async function processSelectedDevices() {
listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
+ lotsSearcher.enable();
} catch (error) {
console.log(error);
listHTML.html("Error feching devices and lots
(see console for more details)");
diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js
index 7a2ecc93..bfd88858 100644
--- a/ereuse_devicehub/static/js/main_inventory.js
+++ b/ereuse_devicehub/static/js/main_inventory.js
@@ -93,7 +93,7 @@ const selectorController = (action) => {
table.on("datatable.perpage", () => itemListCheckChanged());
table.on("datatable.update", () => itemListCheckChanged());
}
-
+
if (action == "softInit") {
softInit();
itemListCheckChanged();
@@ -103,8 +103,8 @@ const selectorController = (action) => {
function itemListCheckChanged() {
alertInfoDevices.innerHTML = `Selected devices: ${TableController.getSelectedDevices().length}
${TableController.getAllDevices().length != TableController.getSelectedDevices().length
- ? `Select all devices (${TableController.getAllDevices().length})`
- : "Cancel selection"
+ ? `Select all devices (${TableController.getAllDevices().length})`
+ : "Cancel selection"
}`;
if (TableController.getSelectedDevices().length <= 0) {
@@ -132,7 +132,7 @@ const selectorController = (action) => {
get_device_list();
}
-
+
btnSelectAll.addEventListener("click", event => {
const checkedState = event.target.checked;
TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => { ckeckbox.checked = checkedState });
@@ -317,6 +317,47 @@ function export_file(type_file) {
}
}
+class lotsSearcher {
+ static lots = [];
+
+ static lotsSearchElement = null;
+
+ static getListLots = () => {
+ const lotsList = document.getElementById("LotsSelector")
+ if (lotsList) {
+ // Apply filter to get only labels
+ return Array.from(lotsList.children).filter(item => item.querySelector("label"));
+ }
+ return [];
+ }
+
+ static enable() {
+ if (this.lotsSearchElement) this.lotsSearchElement.disabled = false;
+ }
+
+ static disable() {
+ if (this.lotsSearchElement) this.lotsSearchElement.disabled = true;
+ }
+
+ /**
+ * do search when lot change in the search input
+ */
+ static doSearch(inputSearch) {
+ const lots = this.getListLots();
+ for (let i = 0; i < lots.length; i++) {
+ if (lot.innerText.toLowerCase().includes(inputSearch.toLowerCase())) {
+ lot.parentElement.style.display = "";
+ } else {
+ lot.parentElement.style.display = "none";
+ }
+ }
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ lotsSearcher.lotsSearchElement = document.getElementById("lots-search");
+ lotsSearcher.lotsSearchElement.addEventListener("input", (e) => { lotsSearcher.doSearch(e.target.value) })
+})
/**
* Reactive lots button
@@ -438,7 +479,7 @@ async function processSelectedDevices() {
const tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest
-
+
const newTable = document.createElement("table")
newTable.innerHTML = tmpDiv.querySelector("table").innerHTML
newTable.classList = "table"
@@ -557,6 +598,7 @@ async function processSelectedDevices() {
document.getElementById("ApplyDeviceLots").classList.add("disabled");
try {
+ lotsSearcher.disable()
listHTML.html("")
const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots();
@@ -589,6 +631,7 @@ async function processSelectedDevices() {
listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
+ lotsSearcher.enable();
} catch (error) {
console.log(error);
listHTML.html("Error feching devices and lots
(see console for more details)");
diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html
index 59050e5a..b5c3ccd1 100644
--- a/ereuse_devicehub/templates/inventory/device_list.html
+++ b/ereuse_devicehub/templates/inventory/device_list.html
@@ -87,7 +87,16 @@
+