summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRenan Rodrigo <rebarbos@redhat.com>2019-09-25 16:05:58 +0200
committerMartin Styk <mastyk@redhat.com>2019-11-07 09:06:24 +0000
commitc26dbf8595458c5b5ded6bcc0328180a7480212c (patch)
tree0cb80056b38a23e998897b79a0ece097f759d446
parent3c9b22194b7fa3a31ec057338375a1bd32193735 (diff)
Rewrite UI of Excluded Families page
Change the big list of checkboxes to be more usable. - Move the buttons from the bottom of the components to the top - Add a filter input, enabling the user to search for specific families - Organize architectures in tabs to make navigation easier - Categorize the available major versions according to their names - Add toggle buttons for architectures and categories, changing visible items - Collapse version specific checkboxes - Fix selecting major and specific versions at the same time - Add 'back to top' navigation button Bug: 719536 Change-Id: Ibf74edd91cf117a77d6b86a7b3a442d05d74a400
-rw-r--r--IntegrationTests/src/bkr/inttest/server/selenium/test_system_view.py38
-rw-r--r--Server/assets/excluded-families.less80
-rw-r--r--Server/assets/style.less1
-rw-r--r--Server/assets/system-exclude.js107
-rw-r--r--Server/bkr/server/assets.py1
-rw-r--r--Server/bkr/server/static/javascript/util.js9
-rw-r--r--Server/bkr/server/widgets.py202
7 files changed, 349 insertions, 89 deletions
diff --git a/IntegrationTests/src/bkr/inttest/server/selenium/test_system_view.py b/IntegrationTests/src/bkr/inttest/server/selenium/test_system_view.py
index 04d9047..dada538 100644
--- a/IntegrationTests/src/bkr/inttest/server/selenium/test_system_view.py
+++ b/IntegrationTests/src/bkr/inttest/server/selenium/test_system_view.py
@@ -25,6 +25,7 @@ from bkr.inttest.server.webdriver_utils import login, check_system_search_result
delete_and_confirm, logout, click_menu_item, BootstrapSelect, wait_for_ajax_loading
from bkr.inttest.server.requests_utils import login as requests_login, patch_json
from selenium.webdriver.support.ui import Select
+from selenium.common.exceptions import ElementNotVisibleException
from bkr.inttest.assertions import wait_for_condition, assert_sorted
class SystemViewTestWD(WebDriverTestCase):
@@ -935,7 +936,7 @@ class SystemViewTestWD(WebDriverTestCase):
self.assertEqual(self.system.mac_address, bad_mac_address)
#https://bugzilla.redhat.com/show_bug.cgi?id=833275
- def test_excluded_families(self):
+ def test_exclude_by_architecture(self):
# Uses the default distro tree which goes by the name
# of DansAwesomeLinux created in setUp()
@@ -951,8 +952,10 @@ class SystemViewTestWD(WebDriverTestCase):
b = self.browser
# simulate the label click for i386
- b.find_element_by_xpath('//li[normalize-space(text())="i386"]'
- '//label[normalize-space(string(.))="DansAwesomeLinux6.9"]').click()
+ # click the major version to open the submenu, and then click the osversion
+ b.find_element_by_xpath('//div[@id="arch-i386"]//i[@data-target="#collapse-i386-DansAwesomeLinux6"]').click()
+ b.find_element_by_xpath('//div[@id="arch-i386"]//span[text()="DansAwesomeLinux6.9"]').click()
+
# Now check if the appropriate checkbox was selected
self.assertTrue(b.find_element_by_xpath(
'//input[@name="excluded_families_subsection.i386" and @value="%s"]'
@@ -966,9 +969,11 @@ class SystemViewTestWD(WebDriverTestCase):
'//input[@name="excluded_families_subsection.i386" and @value="%s"]'
% self.distro_tree.distro.osversion_id).click()
- # simulate the label click for x86_64
- b.find_element_by_xpath('//li[normalize-space(text())="x86_64"]'
- '//label[normalize-space(string(.))="DansAwesomeLinux6.9"]').click()
+ # Change the tab and simulate the label click for x86_64
+ b.find_element_by_id('x86_64-tab').click()
+ b.find_element_by_xpath('//div[@id="arch-x86_64"]//i[@data-target="#collapse-x86_64-DansAwesomeLinux6"]').click()
+ b.find_element_by_xpath('//div[@id="arch-x86_64"]//span[text()="DansAwesomeLinux6.9"]').click()
+
# Now check if the appropriate checkbox was selected
self.assertTrue(b.find_element_by_xpath(
'//input[@name="excluded_families_subsection.x86_64" and @value="%s"]'
@@ -991,6 +996,27 @@ class SystemViewTestWD(WebDriverTestCase):
#assert all clicks are correct
assert all([i.is_selected() for i in checkboxes])
+ def test_exclude_filter(self):
+ # Creates more distro trees to apply the filter on
+ with session.begin():
+ distro_names = [u'RenansAwesomeLinux3', u'AnyOtherLinux0']
+ for distro_name in distro_names:
+ data_setup.create_distro_tree(osmajor=distro_name, osminor=u'2', lab_controllers=[self.lab_controller])
+ self.go_to_system_view(tab='Excluded Families')
+ b = self.browser
+
+ # type a string in the filter input field
+ input_filter = b.find_element_by_css_selector('div.filter input').send_keys('awe')
+ # check if the matching majors are being shown
+ b.find_element_by_xpath('//div[@id="arch-i386"]//i[@data-target="#collapse-i386-DansAwesomeLinux6"]').click()
+ b.find_element_by_xpath('//div[@id="arch-i386"]//i[@data-target="#collapse-i386-RenansAwesomeLinux3"]').click()
+ # check if the non-matching majors are not being shown
+ try:
+ b.find_element_by_xpath('//div[@id="arch-i386"]//i[@data-target="#collapse-i386-AnyOtherLinux0"]').click()
+ except ElementNotVisibleException:
+ return
+ self.fail('Element is displayed but it should not.')
+
def test_can_sort_activity_grid(self):
with session.begin():
self.system.record_activity(service=u'testdata', field=u'status_reason',
diff --git a/Server/assets/excluded-families.less b/Server/assets/excluded-families.less
new file mode 100644
index 0000000..d87b857
--- /dev/null
+++ b/Server/assets/excluded-families.less
@@ -0,0 +1,80 @@
+
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+
+.excludedfamilies{
+ width: 80%;
+ max-width: 750px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 20px;
+ position: relative;
+
+ div.filter{
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 300px;
+ display: flex;
+ flex-direction: column;
+
+ input{
+ width: 100%;
+ }
+ }
+
+ ul.available-archs{
+ width: 100%;
+ }
+
+ .archs-list{
+ width: 100%;
+ overflow: visible;
+
+ .excluded-families-title{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 5px 10px;
+ }
+
+ .arch-title{
+ background-color: #f1f1f1;
+ border-top: 2px solid #cccccc;
+ }
+
+ .category-list{
+ padding: 0 0 30px 15px;
+
+ .category-title{
+ background-color: #f6f6f6;
+ border-top: 1px solid #cccccc;
+ }
+
+ ul.os-version-list.in{
+ padding-top: 5px;
+ }
+ }
+
+ label.with-arrow{
+ display: inline;
+ }
+
+ i{
+ cursor: pointer;
+ }
+
+ }
+
+ .back-to-top{
+ display: none;
+ align-self: flex-end;
+ position: sticky;
+ bottom: 30px;
+ z-index: 10;
+ }
+}
diff --git a/Server/assets/style.less b/Server/assets/style.less
index 48a8475..2d31da1 100644
--- a/Server/assets/style.less
+++ b/Server/assets/style.less
@@ -405,6 +405,7 @@ input.tt-hint {
@import "recipe.less";
@import "job-and-recipe.less";
@import "reservation-duration-selection.less";
+@import "excluded-families.less";
// Site-specific styles. The beaker-server package creates site.less as
// a symlink to /dev/null if it doesn't exist. Admins can overwrite the symlink
diff --git a/Server/assets/system-exclude.js b/Server/assets/system-exclude.js
new file mode 100644
index 0000000..930a85b
--- /dev/null
+++ b/Server/assets/system-exclude.js
@@ -0,0 +1,107 @@
+function toggleExcludeAll(){
+ var major = $(".majorCheckbox");
+ major[0].checked = !major[0].checked;
+ for(var i = 1; i < major.length; i++){
+ major[i].checked = major[0].checked;
+ major[i].indeterminate = false;
+ }
+}
+
+function toggleExclude(arch, value){
+ searchString = arch;
+ if(value){
+ searchString += ("_" +value);
+ }
+ var major = $("[id*="+searchString+"].majorCheckbox:not(.filtered-out)");
+ major[0].checked = !major[0].checked;
+ for(var i = 1; i < major.length; i++){
+ major[i].checked = major[0].checked;
+ checkMajor(major[i]);
+ major[i].indeterminate = false;
+ }
+}
+
+function filterFamilies(filterString){
+ var labels = $(".excludedfamilies label");
+ for(var i=0; i<labels.length;i++){
+ if(labels[i].children[1].innerHTML.toLowerCase().search(filterString.toLowerCase()) == -1){
+ labels[i].parentElement.style.display = "none";
+ labels[i].children[0].classList.add("filtered-out");
+ }
+ else{
+ labels[i].parentElement.style.display = "";
+ labels[i].children[0].classList.remove("filtered-out");
+ }
+ }
+}
+
+function preventSubmit(event){
+ if(event.key == "Enter"){
+ event.preventDefault();
+ }
+}
+
+function _findParentElement(element, parentTag){
+ if(element.parentNode.tagName == parentTag){
+ return element.parentNode;
+ }
+ return _findParentElement(element.parentNode, parentTag);
+}
+
+function checkMajor(checkBox){
+ if(checkBox.checked){
+ var parentDiv = _findParentElement(checkBox, "DIV");
+ var versions = parentDiv.querySelectorAll("input:not(.majorCheckbox)");
+ for(var i=0; i<versions.length; i++){
+ versions[i].checked = false;
+ }
+ }
+}
+
+function checkVersion(checkBox){
+ var parentDiv = _findParentElement(checkBox, "DIV");
+ var major = parentDiv.querySelector("input.majorCheckbox");
+ var versions = parentDiv.querySelectorAll("input:not(.majorCheckbox)");
+ if(checkBox.checked){
+ major.checked = false;
+ major.indeterminate = true;
+ }
+ else{
+ for(var i=0; i<versions.length; i++){
+ if(versions[i].checked){
+ return;
+ }
+ }
+ major.indeterminate = false;
+ }
+}
+
+function initializeExcludedFamilies(){
+ var majors = document.querySelector(".archs-list").querySelectorAll("input.majorCheckbox");
+ var versions = document.querySelector(".archs-list").querySelectorAll("input:not(.majorCheckbox)");
+ for(var i=0; i<majors.length; i++){
+ checkMajor(majors[i]);
+ }
+ for(var i=0; i<versions.length; i++){
+ checkVersion(versions[i]);
+ }
+
+ $('.nav-tabs li:first-child a').tab('show');
+}
+
+function backToTop(){
+ document.body.scrollTop = 0;
+ document.documentElement.scrollTop = 0;
+}
+
+function backToTopShow(event){
+ if(event.target.URL.search("#exclude") != -1){
+ if (document.body.scrollTop > 350 || document.documentElement.scrollTop > 350) {
+ document.querySelector(".back-to-top").style.display = "block";
+ } else {
+ document.querySelector(".back-to-top").style.display = "";
+ }
+ }
+}
+
+window.addEventListener("scroll", backToTopShow); \ No newline at end of file
diff --git a/Server/bkr/server/assets.py b/Server/bkr/server/assets.py
index ca0225e..f940f95 100644
--- a/Server/bkr/server/assets.py
+++ b/Server/bkr/server/assets.py
@@ -99,6 +99,7 @@ def _create_env(source_dir, output_dir, **kwargs):
'system-add.js',
'system-access-policy.js',
'system-commands.js',
+ 'system-exclude.js',
'system-executed-tasks.js',
'system-hardware.js',
'system-loan.js',
diff --git a/Server/bkr/server/static/javascript/util.js b/Server/bkr/server/static/javascript/util.js
index 4302771..70eea59 100644
--- a/Server/bkr/server/static/javascript/util.js
+++ b/Server/bkr/server/static/javascript/util.js
@@ -109,12 +109,3 @@ function system_action_remote_form_request(form, options, action) {
remoteRequest(form, action, null, query, options);
return true;
}
-
-function excludeAll(){
- var major = $(".majorCheckbox");
- major[0].checked = !major[0].checked;
- for(var i = 1; i < major.length; i++){
- major[i].checked = major[0].checked;
- }
-}
-
diff --git a/Server/bkr/server/widgets.py b/Server/bkr/server/widgets.py
index e40aaea..884f5e9 100644
--- a/Server/bkr/server/widgets.py
+++ b/Server/bkr/server/widgets.py
@@ -738,55 +738,110 @@ class LabInfoForm(HorizontalForm):
class ExcludedFamilies(FormField):
template = """
- <ul xmlns:py="http://purl.org/kid/ns#"
- class="${field_class}"
- id="${field_id}"
- py:attrs="list_attrs"
- >
- <li py:for="arch, a_options in options">
- ${arch}
- <ul xmlns:py="http://purl.org/kid/ns#"
- class="${field_class}"
- id="${field_id}_${arch}"
- py:attrs="list_attrs"
- >
- <li py:for="value, desc, subsection, attrs in a_options">
- <label class="checkbox">
- <input class="majorCheckbox"
- type="checkbox"
- name="${name}.${arch}"
- id="${field_id}_${value}_${arch}"
- value="${value}"
- py:attrs="attrs"
- />
- ${desc}
- </label>
- <ul xmlns:py="http://purl.org/kid/ns#"
- class="${field_class}"
- id="${field_id}_${value}_sub"
- py:attrs="list_attrs"
- >
- <li py:for="subvalue, subdesc, attrs in subsection">
- <label class="checkbox">
- <input type="checkbox"
- name="${name}_subsection.${arch}"
- id="${field_id}_${value}_sub_${subvalue}_${arch}"
- value="${subvalue}"
- py:attrs="attrs"
- />
- ${subdesc}
- </label>
- </li>
+ <div xmlns:py="http://purl.org/kid/ns#"
+ class="${field_class}"
+ id="${field_id}"
+ >
+ <div class="filter">
+ <strong>Filter by OS family:</strong>
+ <input
+ placeholder="Type here to apply filter"
+ onkeyup="filterFamilies(event.target.value)"
+ onkeydown="preventSubmit(event)"
+ />
+ </div>
+ <p><strong>Available architectures:</strong></p>
+ <ul class="nav nav-tabs available-archs" role="tablist">
+ <li py:for="arch, _ in options">
+ <a href="#arch-${arch}"
+ class="nav-link"
+ id="${arch}-tab"
+ data-toggle="tab"
+ >
+ ${arch}
+ </a>
+ </li>
</ul>
- </li>
- </ul>
- </li>
- </ul>
+ <div class="tab-content archs-list">
+ <div id="arch-${arch}" class="tab-pane fade" py:for="arch, a_options in options">
+ <div class="arch-title excluded-families-title">
+ <span>Architecture: ${arch}</span>
+ <button type="button"
+ class="btn"
+ onclick="toggleExclude('${arch}')"
+ >
+ Toggle ${arch}
+ </button>
+ </div>
+ <div class="category-list" py:for="category, cat_options in a_options">
+ <div class="category-title excluded-families-title">
+ <span>${category}</span>
+ <button type="button"
+ class="btn"
+ onclick="toggleExclude('${arch}','${category}')"
+ >
+ Toggle
+ </button>
+ </div>
+ <div py:for="value, desc, subsection, attrs in cat_options">
+ <label class="with-arrow">
+ <input class="majorCheckbox"
+ type="checkbox"
+ name="${name}.${arch}"
+ id="${field_id}_${value}_${arch}_${category}"
+ value="${value}"
+ py:attrs="attrs"
+ onchange="checkMajor(this)"
+ />
+ <span>${desc}</span>
+ </label>
+ <i class="fa fa-angle-down" data-toggle="collapse" data-target="#collapse-${arch}-${desc}"></i>
+ <ul class="collapse os-version-list"
+ id="collapse-${arch}-${desc}"
+ >
+ <li py:for="subvalue, subdesc, attrs in subsection">
+ <label>
+ <input type="checkbox"
+ name="${name}_subsection.${arch}"
+ id="${field_id}_${value}_sub_${subvalue}_${arch}"
+ value="${subvalue}"
+ py:attrs="attrs"
+ onchange="checkVersion(this)"
+ />
+ <span>${subdesc}</span>
+ </label>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="back-to-top">
+ <button type="button"
+ onclick="backToTop()"
+ >
+ Back to Top
+ <i class="fa fa-arrow-up"></i>
+ </button>
+ </div>
+ <script>
+ initializeExcludedFamilies();
+ </script>
+ </div>
"""
_multiple_selection = True
_selected_verb = 'checked'
+ _major_categories = [
+ {
+ 'name': 'RHEL',
+ 'match': 'RedHatEnterprise'
+ },{
+ 'name': 'Fedora',
+ 'match': 'Fedora'
+ }
+ ]
params = ["attrs", "options", "list_attrs"]
params_doc = {'list_attrs' : 'Extra (X)HTML attributes for the ul tag'}
list_attrs = {}
@@ -801,35 +856,34 @@ class ExcludedFamilies(FormField):
super(ExcludedFamilies, self).update_params(d)
a_options = []
for arch,arch_options in d["options"]:
- options = []
- for optgroup in arch_options:
- optlist = [optgroup]
+ options = { name: [] for name in [category['name'] for category in self._major_categories]}
+ options['Other'] = []
+ for option in arch_options:
soptions = []
- for i, option in enumerate(optlist):
- if len(option) is 3:
- option_attrs = {}
- elif len(option) is 4:
- option_attrs = dict(option[3])
+ option_attrs = dict(option[3]) if len(option) == 4 else {}
+ if d['attrs'].has_key('readonly'):
+ option_attrs['readonly'] = 'readonly'
+ if self._is_selected(option[0], d['value'][0][arch]):
+ option_attrs[self._selected_verb] = self._selected_verb
+ for soption in option[2]:
+ soption_attrs = dict(soption[2]) if len(soption) == 3 else {}
if d['attrs'].has_key('readonly'):
- option_attrs['readonly'] = 'True'
- if self._is_selected(option[0], d['value'][0][arch]):
- option_attrs[self._selected_verb] = self._selected_verb
- for soptgroup in option[2]:
- soptlist = [soptgroup]
- for j, soption in enumerate(soptlist):
- if len(soption) is 2:
- soption_attrs = {}
- elif len(soption) is 3:
- soption_attrs = dict(soption[2])
- if d['attrs'].has_key('readonly'):
- soption_attrs['readonly'] = 'True'
- if self._is_selected(soption[0], d['value'][1][arch]):
- soption_attrs[self._selected_verb] = self._selected_verb
- soptlist[j]=(soption[0], soption[1], soption_attrs)
- soptions.extend(soptlist)
- optlist[i] = (option[0], option[1], soptions, option_attrs)
- options.extend(optlist)
- a_options.append((arch,options))
+ soption_attrs['readonly'] = 'readonly'
+ if self._is_selected(soption[0], d['value'][1][arch]):
+ soption_attrs[self._selected_verb] = self._selected_verb
+ soptions.append((soption[0], soption[1], soption_attrs))
+ option_category = 'Other'
+ for category in self._major_categories:
+ if option[1].find(category['match']) == 0:
+ option_category = category['name']
+ options[option_category].append((option[0], option[1], soptions, option_attrs))
+ for category in options:
+ options[category].sort(key=lambda o: o[1])
+ ordered_categories = []
+ for category in self._major_categories:
+ ordered_categories.append((category['name'], options[category['name']]))
+ ordered_categories.append(('Other', options['Other']))
+ a_options.append((arch, ordered_categories))
d["options"] = a_options
def _is_selected(self, option_value, value):
@@ -932,10 +986,10 @@ class SystemExclude(Form):
name="${name}"
action="${action}"
method="post" width="100%">
- ${display_field_for("id")}
- ${display_field_for("excluded_families")}
- <button id="excludeButton" type="button" py:if="not readonly" class="btn" onclick='excludeAll();'>Exclude All</button>
- <a py:if="not readonly" class="btn btn-primary" href="javascript:document.${name}.submit();">Save Exclude Changes</a>
+ <button id="excludeButton" type="button" py:if="not readonly" class="btn" onclick='toggleExcludeAll();'><strong>Toggle All Architectures/Families</strong></button>
+ <a py:if="not readonly" class="btn btn-primary" href="javascript:document.${name}.submit();">Save Exclude Changes</a>
+ ${display_field_for("id")}
+ ${display_field_for("excluded_families")}
</form>
"""
member_widgets = ["id", "excluded_families"]