diff --git a/src/test/unit/web_interface/test_app_re_analyze.py b/src/test/unit/web_interface/test_app_re_analyze.py index 8d01f1e3d..abc231b9b 100644 --- a/src/test/unit/web_interface/test_app_re_analyze.py +++ b/src/test/unit/web_interface/test_app_re_analyze.py @@ -1,3 +1,5 @@ +import re + from helperFunctions.data_conversion import make_bytes from test.common_helper import TEST_FW @@ -12,11 +14,11 @@ def test_app_re_analyze_get_valid_firmware(self, test_client): assert b'

update analysis of

' in rv.data assert b'
TEST_FW_HID
' in rv.data assert ( - b'value="default_plugin" checked' not in rv.data + re.search(rb'value="default_plugin"\s+checked>', rv.data) is None ), 'plugins that did not run for TEST_FW should not be checked' assert b'value="mandatory_plugin"' not in rv.data, 'mandatory plugins should not be listed' - assert ( - b'value="optional_plugin" checked' in rv.data + assert re.search( + rb'value="optional_plugin"\s+checked>', rv.data ), 'optional plugins that did run for TEST_FW should be checked' def test_app_re_analyze_post_valid(self, test_client, intercom_task_list): diff --git a/src/test/unit/web_interface/test_app_upload.py b/src/test/unit/web_interface/test_app_upload.py index 6b7b68b0e..4f73de688 100644 --- a/src/test/unit/web_interface/test_app_upload.py +++ b/src/test/unit/web_interface/test_app_upload.py @@ -1,3 +1,4 @@ +import re from io import BytesIO @@ -5,9 +6,9 @@ class TestAppUpload: def test_app_upload_get(self, test_client): rv = test_client.get('/upload') assert b'

Upload Firmware

' in rv.data - assert b'value="default_plugin" checked' in rv.data + assert re.search(rb'value="default_plugin"\s+checked>', rv.data) assert b'value="mandatory_plugin"' not in rv.data - assert b'value="optional_plugin" >' in rv.data + assert re.search(rb'value="optional_plugin"\s+>', rv.data) def test_app_upload_invalid_firmware(self, test_client, intercom_task_list): rv = test_client.post( diff --git a/src/web_interface/static/css/upload.css b/src/web_interface/static/css/upload.css index 1a261ce0b..691568c4e 100644 --- a/src/web_interface/static/css/upload.css +++ b/src/web_interface/static/css/upload.css @@ -1,7 +1,21 @@ -.search { - text-align: right; +.upload-label { + justify-content: center; + width: 135px; +} +.upload-icon { + justify-content: center; + width: 45px; +} +.autocomplete-items { + position: absolute; + z-index: 99; + top: 100%; + left: 135px; /* upload-label width */ + right: 35px; /* dropdown button width */ + font-family: monospace; +} +.autocomplete-items li { + cursor: pointer; + padding: 5px 5px 5px 10px; + border: none; } -.btn-file { - position: relative; - overflow: hidden; -} \ No newline at end of file diff --git a/src/web_interface/static/js/upload.js b/src/web_interface/static/js/upload.js index 530b55f5e..fd5e71294 100644 --- a/src/web_interface/static/js/upload.js +++ b/src/web_interface/static/js/upload.js @@ -5,39 +5,30 @@ $(() => { }); }); -function add_device_class_options(selected_device_class, selected_vendor, data) { - const deviceClassButton = document.getElementById("device_class_select_button"); - let deviceNameList = $('#device_name_list'); - deviceNameList.empty(); - if (data.hasOwnProperty(selected_device_class)) { - if (data[selected_device_class].hasOwnProperty(selected_vendor)) { - let device_classes = data[selected_device_class][selected_vendor]; - // remove duplicates - device_classes = [...new Set(device_classes)]; - device_classes.sort(); - if (device_classes.length > 0) { - for (let index in device_classes) { - deviceNameList.append(` - - ${device_classes[index]} - - `); - } - deviceClassButton.disabled = false; +function addDeviceNameOptions(selected_device_class, selected_vendor) { + const dropdownButton = document.getElementById("device_name-button"); + if (allDeviceNames.hasOwnProperty(selected_device_class)) { + if (allDeviceNames[selected_device_class].hasOwnProperty(selected_vendor)) { + // update the global variable but without overwriting it + deviceNames.length = 0; // empty existing array + deviceNames.push(...new Set(allDeviceNames[selected_device_class][selected_vendor])); // remove duplicates + if (deviceNames.length > 0) { + deviceNames.sort(); + dropdownButton.disabled = false; return; } } } - deviceClassButton.disabled = true; + dropdownButton.disabled = true; } -function update_device_names() { +function updateDeviceNames() { const deviceClassInput = document.getElementById("device_class"); const vendorInput = document.getElementById("vendor"); const vendor = vendorInput.value.trim(); const device_class = deviceClassInput.value.trim(); if (vendor.length > 0 && device_class.length > 0) { - add_device_class_options(device_class, vendor, device_names); + addDeviceNameOptions(device_class, vendor); } } @@ -52,21 +43,113 @@ function change_selected_plugins(preset_name) { } } -function updateInput(input_id, element, do_update = false) { - const input = document.getElementById(input_id); - input.value = element.innerText; - if (do_update) { - update_device_names(); +function autocompleteInput(inputId, options) { + const input = document.getElementById(inputId); + const dropdownButton = document.getElementById(`${inputId}-button`); + let currentFocus; + + input.addEventListener("input", populateAutocomplete); + dropdownButton.addEventListener("click", () => { + input.value = ""; + populateAutocomplete(); + }); + + input.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.code === "Space") { + populateAutocomplete(); + return; + } else if (event.code === "Tab") { + closeAll(); + return; + } + let list = document.getElementById(input.id + "-autocomplete-list"); + if (!list) return; + let listElements = list.getElementsByTagName("li"); + if (event.code === "ArrowDown") { + currentFocus++; + setActive(listElements); + } else if (event.code === "ArrowUp") { + currentFocus--; + setActive(listElements); + } else if (event.code === "Enter") { + event.preventDefault(); // prevent the form from being submitted + if (currentFocus > -1) { + listElements[currentFocus].click(); // simulate a click on the item + } + } + }); + + function populateAutocomplete() { + closeAll(); + currentFocus = -1; + const list = document.createElement("ul"); + list.setAttribute("id", input.id + "-autocomplete-list"); + list.setAttribute("class", "autocomplete-items list-group border"); + input.parentNode.appendChild(list); + + let listItem, target; + options.forEach(option => { + let index = option.toLowerCase().indexOf(input.value.toLowerCase()); + if (!input.value || index !== -1) { + listItem = document.createElement("li"); + listItem.setAttribute("class", "list-group-item list-group-item-action"); + listItem.innerHTML = ( + option.slice(0, index) + + // display the matched part in bold (with the class click-through, the event.target in the + // eventListener below will not be the element) + `${option.slice(index, index + input.value.length)}` + + option.slice(index + input.value.length) + ); + listItem.__value = option; + listItem.__inputId = inputId; + listItem.addEventListener("click", (event) => { + input.value = event.target.__value; + closeAll(); + if (["device_class", "vendor"].includes(event.target.__inputId)) { + updateDeviceNames(); + } + }); + list.appendChild(listItem); + } + }); + } + + function setActive(elements) { + if (elements === null || elements.length === 0) return false; + Array.from(elements).forEach(element => { + element.classList.remove("active"); + }); + if (currentFocus >= elements.length) { + currentFocus = 0; + } else if (currentFocus < 0) { + currentFocus = (elements.length - 1); + } + elements[currentFocus].classList.add("active"); } } -function filterFunction(input) { - let filter = input.value.toLowerCase(); - input.parentElement.querySelectorAll(".dropdown-item").forEach(element => { - if (!element.innerText.toLowerCase().includes(filter)) { - element.style.display = "none"; - } else { - element.style.display = ""; +function closeAll(currentElement) { + if (currentElement && currentElement.id.endsWith("-button")) { + // if we clicked on the dropdown button we don't want to close the autocomplete list (we just opened) + currentElement = getAutocompleteElement(currentElement.parentNode.parentNode); + } else if (currentElement && currentElement.tagName.toLowerCase() === "input") { + // also don't close the autocomplete that belongs to an input we just clicked + currentElement = getAutocompleteElement(currentElement.parentNode); + } + const elements = document.getElementsByClassName("autocomplete-items"); + Array.from(elements).forEach((element) => { + if (currentElement !== element) { + element.parentNode.removeChild(element); } }); } + +function getAutocompleteElement(parentElement) { + const autocompletes = parentElement.querySelectorAll("ul"); + if (autocompletes.length === 1) return autocompletes[0]; + return null; +} + +document.addEventListener("click", (event) => { + closeAll(event.target); +}); \ No newline at end of file diff --git a/src/web_interface/templates/macros.html b/src/web_interface/templates/macros.html index 217d3684e..bcb9ea8f3 100644 --- a/src/web_interface/templates/macros.html +++ b/src/web_interface/templates/macros.html @@ -22,38 +22,24 @@
{{ panel_title }}< {%- endmacro %} -{% macro upload_input(key, label) %} +{% macro upload_input(key, label, dropdown=False) %}
-
{{ caller() }} -
-{%- endmacro %} -{% macro upload_dropdown(input_id, options, update_device_list=False, include_filter=False) %} -
- - {% if include_filter %} - {%- endmacro %} diff --git a/src/web_interface/templates/upload/upload.html b/src/web_interface/templates/upload/upload.html index bd7b8c190..110ef08b2 100644 --- a/src/web_interface/templates/upload/upload.html +++ b/src/web_interface/templates/upload/upload.html @@ -4,36 +4,37 @@ {%- set active_page = "Upload" -%} {%- set is_reanalysis = True if firmware else False -%} +{%- set device_part_options = ["complete", "kernel", "bootloader", "root-fs"] -%} {% block head %} - - {% endblock %} {%- block styles -%} + {%- endblock -%} {%- block body -%} @@ -77,47 +78,31 @@

Upload Firmware

{# Device Class #} - {%- call macros.upload_input("device_class", "Device Class") %} + {%- call macros.upload_input("device_class", "Device Class", True) %} - - {{ macros.upload_dropdown("device_class", device_classes, True, True) }} + placeholder="Select device class..." autocomplete="off" required> {%- endcall %} {# Vendor #} - {%- call macros.upload_input("vendor", "Vendor") %} + {%- call macros.upload_input("vendor", "Vendor", True) %} - - {{ macros.upload_dropdown("vendor", vendors, True, True) }} + placeholder="Select vendor..." autocomplete="off" required> {%- endcall %} {# Device Name #} - {%- call macros.upload_input("device_name", "Device Name") %} + {%- call macros.upload_input("device_name", "Device Name", True) %} - -
- - -
+ placeholder="Select device name..." autocomplete="off" required> {%- endcall %} {# Device Part #} - {%- call macros.upload_input("device_part", "Device Part") %} + {%- call macros.upload_input("device_part", "Device Part", True) %} - - {{ macros.upload_dropdown("device_part", ["complete", "kernel", "bootloader", "root-fs"]) }} {%- endcall %} {# Version #} @@ -160,7 +145,8 @@

Upload Firmware

@@ -193,7 +179,7 @@

Upload Firmware

+ alt="loading..." style="display: none; margin: auto;"/>