From 5ec5db313f8edbca3b77551fad04ecd2f15398f3 Mon Sep 17 00:00:00 2001 From: Christian Bruckmayer Date: Tue, 27 Nov 2018 13:01:52 +0100 Subject: [PATCH 1/3] Implement Project#monitor page in bootstrap The project build montior table is now a DataTable and all filter / search is performed in the DataTable now instead of on the Server. The monitor action still allows server filtering by passing parameters. We still need to support this because of existing links. However, when we already filtered on server side, the rendered table would only contain a the filtered list which causes the client search to not work as expected. For now, we just disable the client search in this case and show a button to remove all server side filters. This does a simple roundtrip to the server without parameters which enables us to activate the client search again. --- .../assets/javascripts/webui2/application.js | 1 + .../assets/javascripts/webui2/datatables.js | 3 +- .../javascripts/webui2/project_monitor.js | 105 ++++++++++++++++++ .../assets/stylesheets/webui2/datatables.scss | 1 + .../controllers/webui/project_controller.rb | 9 ++ .../webui/project/_monitor_control.html.haml | 58 ++++++++++ .../webui2/webui/project/_tabs.html.haml | 2 + .../webui2/webui/project/monitor.html.haml | 37 ++++++ 8 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/api/app/assets/javascripts/webui2/project_monitor.js create mode 100644 src/api/app/views/webui2/webui/project/_monitor_control.html.haml create mode 100644 src/api/app/views/webui2/webui/project/monitor.html.haml diff --git a/src/api/app/assets/javascripts/webui2/application.js b/src/api/app/assets/javascripts/webui2/application.js index b38215c100b..11a577038e9 100644 --- a/src/api/app/assets/javascripts/webui2/application.js +++ b/src/api/app/assets/javascripts/webui2/application.js @@ -41,3 +41,4 @@ //= require webui2/jquery-ui.min.js //= require webui2/cm2/use-codemirror.js //= require webui2/package-view_file.js +//= require webui2/project_monitor.js diff --git a/src/api/app/assets/javascripts/webui2/datatables.js b/src/api/app/assets/javascripts/webui2/datatables.js index 2d540e13a41..2df70015e65 100644 --- a/src/api/app/assets/javascripts/webui2/datatables.js +++ b/src/api/app/assets/javascripts/webui2/datatables.js @@ -2,6 +2,8 @@ //= require datatables/dataTables.bootstrap4 //= require datatables/extensions/Responsive/dataTables.responsive //= require datatables/extensions/Responsive/responsive.bootstrap4 +//= require datatables/extensions/FixedColumns/fixedColumns.bootstrap4 +//= require datatables/extensions/FixedColumns/dataTables.fixedColumns function initializeDataTable(cssSelector, params){ // jshint ignore:line var defaultParams = { @@ -10,4 +12,3 @@ function initializeDataTable(cssSelector, params){ // jshint ignore:line var newParams = $.extend(defaultParams, params); $(cssSelector).DataTable(newParams); } - diff --git a/src/api/app/assets/javascripts/webui2/project_monitor.js b/src/api/app/assets/javascripts/webui2/project_monitor.js new file mode 100644 index 00000000000..b95b823da80 --- /dev/null +++ b/src/api/app/assets/javascripts/webui2/project_monitor.js @@ -0,0 +1,105 @@ +function hideRepositoryColumns() { + var table = $('#project-monitor-table').DataTable(); + + var repositories = []; + $('#project-monitor-repositories-dropdown input:checked').each(function () { + repositories.push($(this).val().trim()); + }); + + if (repositories.length === 0) { + return; + } + + var toShowColumns = []; + var offSet = 0; + $('#project-monitor-table thead tr:eq(0) th').each(function () { + var column = $(this); + var colSpan = column.prop('colspan'); + if (repositories.includes(column.text().trim())) { + var range = []; + for (var i = offSet; i < offSet + colSpan; i++) { + range.push(i); + } + toShowColumns = toShowColumns.concat(range); + } + offSet += colSpan; + }); + + table.columns().every(function () { + var index = this.index(); + if (index === 0) return; + this.visible(toShowColumns.includes(index)); + }); +} + +function hideArchitectureColumns() { + var table = $('#project-monitor-table').DataTable(); + + var architectures = []; + $('#project-monitor-architectures-dropdown input:checked').each(function () { + architectures.push($(this).val()); + }); + + if (architectures.length === 0) { + return; + } + + table.columns().every(function () { + if (this.index() === 0) return; + if (!this.visible()) return; + + var title = $(this.header()).text().trim(); + this.visible(architectures.includes(title)); + }); +} + +function updateStatusSearch() { + var table = $('#project-monitor-table').DataTable(); + var searchInput = $('#project-monitor_filter input'); + + var terms = []; + $('#project-monitor-status-dropdown input:checked').each(function () { + terms.push($(this).val()); + }); + + var searchTerm = terms.join("|"); + searchInput.val(searchTerm); + table.search(searchTerm).draw(); +} + +function updateMonitorFilters() { + updateStatusSearch(); + var table = $('#project-monitor-table').DataTable(); + table.columns().visible(true); + hideRepositoryColumns(); + hideArchitectureColumns(); +} + +function setupProjectMonitor() { // jshint ignore:line + initializeDataTable('#project-monitor-table', { // jshint ignore:line + scrollX: true, + scrollY: "50vh", + fixedColumns: true, + pageLength: 50, + search: { + regex: true, + smart: false, + } + }); + + $('[data-toggle="popover"]').popover({ trigger: 'hover click' }); + + $('#project-monitor-status-dropdown input:checkbox').on('change', function () { + updateStatusSearch(); + }); + + $('.monitor-no-filter-link').on('click', function () { + $(this).siblings().children('input:checked').prop('checked', false); + updateMonitorFilters(); + + }); + + $('#project-monitor-architectures-dropdown input:checkbox, #project-monitor-repositories-dropdown input:checkbox').on('change', function () { + updateMonitorFilters(); + }); +} \ No newline at end of file diff --git a/src/api/app/assets/stylesheets/webui2/datatables.scss b/src/api/app/assets/stylesheets/webui2/datatables.scss index ed8f4171a5f..73b61a2c525 100644 --- a/src/api/app/assets/stylesheets/webui2/datatables.scss +++ b/src/api/app/assets/stylesheets/webui2/datatables.scss @@ -1,5 +1,6 @@ @import 'datatables/dataTables.bootstrap4'; @import 'datatables/extensions/Responsive/responsive.bootstrap4'; +@import 'datatables/extensions/FixedColumns/fixedColumns.bootstrap4'; table.table-fixed { table-layout: fixed diff --git a/src/api/app/controllers/webui/project_controller.rb b/src/api/app/controllers/webui/project_controller.rb index db054be70c7..e3dc97afa35 100644 --- a/src/api/app/controllers/webui/project_controller.rb +++ b/src/api/app/controllers/webui/project_controller.rb @@ -396,6 +396,14 @@ def move_path end def monitor + if params.keys.length < 4 + @activate_client_search = true + else + flash[:notice] = "Limited search results, click #{view_context.link_to('here', project_monitor_path(@project))} to remove the filter." + end + + @legend = Buildresult::STATUS_DESCRIPTION + @name_filter = params[:pkgname] @lastbuild_switch = params[:lastbuild] if params[:defaults] @@ -454,6 +462,7 @@ def monitor @repohash[repo].delete(arch) unless has_packages end end + switch_to_webui2 end # should be in the package controller, but all the helper functions to render the result of a build are in the project diff --git a/src/api/app/views/webui2/webui/project/_monitor_control.html.haml b/src/api/app/views/webui2/webui/project/_monitor_control.html.haml new file mode 100644 index 00000000000..e6e2750be5c --- /dev/null +++ b/src/api/app/views/webui2/webui/project/_monitor_control.html.haml @@ -0,0 +1,58 @@ +.row + .col-md-12 + - if activate_client_search + %span.dropdown#project-monitor-status-dropdown + %button.btn.btn-outline-secondary.dropdown-toggle{ data: { toggle: :dropdown }, type: :button } + %span.caret + Status + .dropdown-menu + %button.btn.btn-link.monitor-no-filter-link No filter + - status.each do |status| + .custom-control.custom-checkbox.dropdown-item.ml-2 + %input.custom-control-input{ type: :checkbox, id: "#{status}-checkbox".parameterize, checked: false, value: status } + %label.custom-control-label{ for: "#{status}-checkbox".parameterize } + = status + %span.dropdown#project-monitor-architectures-dropdown + %button.btn.btn-outline-secondary.dropdown-toggle{ data: { toggle: :dropdown }, type: :button } + %span.caret + Architecture + .dropdown-menu + %button.btn.btn-link.monitor-no-filter-link No filter + - architectures.each do |architecture| + .custom-control.custom-checkbox.dropdown-item.ml-2 + %input.custom-control-input{ type: :checkbox, id: "#{architecture}-checkbox".parameterize, checked: false, value: architecture } + %label.custom-control-label{ for: "#{architecture}-checkbox".parameterize } + = architecture + %span.dropdown#project-monitor-repositories-dropdown + %button.btn.btn-outline-secondary.dropdown-toggle{ data: { toggle: :dropdown }, type: :button } + %span.caret + Repository + .dropdown-menu + %button.btn.btn-link.monitor-no-filter-link No filter + - repositories.each do |repository| + .custom-control.custom-checkbox.dropdown-item.ml-2 + %input.custom-control-input{ type: :checkbox, id: "#{repository}-checkbox".parameterize, checked: false, value: repository } + %label.custom-control-label{ for: "#{repository}-checkbox".parameterize } + = repository + %button.btn.btn-outline-secondary{ data: { toggle: 'modal', target: '#build-monitor-legend' }, title: 'Build status legend' } + Legend + - unless activate_client_search + .float-right + %a.btn.btn-outline-primary{ href: project_monitor_path(project) } + Remove filter + +.modal.fade#build-monitor-legend{ tabindex: -1, role: 'dialog', aria: { labelledby: 'confirm-modal-label', hidden: true } } + .modal-dialog.modal-dialog-centered.modal-lg{ role: 'document' } + .modal-content + .modal-header + %h5.modal-title + Build status legend + .modal-body + - @legend.each do |status, description| + %p + %strong + #{status}: + = description + .modal-footer + %button.btn.btn-sm.btn-outline-secondary.px-4{ data: { dismiss: 'modal' } } + Cancel diff --git a/src/api/app/views/webui2/webui/project/_tabs.html.haml b/src/api/app/views/webui2/webui/project/_tabs.html.haml index 2ecca04ac58..5031d3aa0bd 100644 --- a/src/api/app/views/webui2/webui/project/_tabs.html.haml +++ b/src/api/app/views/webui2/webui/project/_tabs.html.haml @@ -11,6 +11,8 @@ - unless project.defines_remote_instance? || project.is_maintenance? %li.nav-item = tab_link('Repositories', repositories_path) + %li.nav-item + = tab_link('Monitor', project_monitor_path(project)) %li.nav-item = tab_link('Requests', project_requests_path) - unless project.defines_remote_instance? diff --git a/src/api/app/views/webui2/webui/project/monitor.html.haml b/src/api/app/views/webui2/webui/project/monitor.html.haml new file mode 100644 index 00000000000..795356d72bd --- /dev/null +++ b/src/api/app/views/webui2/webui/project/monitor.html.haml @@ -0,0 +1,37 @@ +:ruby + @pagetitle = "Show #{@project}" + @layouttype = 'custom' + +.card.mb-3 + = render partial: 'tabs', locals: { project: @project } + .card-body#project-monitor + = render partial: 'monitor_control', + locals: { project: @project, activate_client_search: @activate_client_search, + status: @avail_status_values, repositories: @avail_repo_values, architectures: @avail_arch_values } + .row.mt-4 + .col-md-12.obs-dataTable + %table.table.table-striped.table-bordered.text-nowrap.w-100#project-monitor-table + %thead.header + %tr + %th + - @repohash.sort.each do |repo, archlist| + - next if archlist.empty? + %th.text-center{ colspan: archlist.length } + = repo + %tr + %th + - @repohash.sort.each do |_repo, archlist| + - archlist.sort.each do |arch| + %th + = arch + %tbody + - @packagenames.each do |packname| + %tr + %td + = link_to word_break(packname, 40), controller: :package, action: :show, package: packname, project: @project.to_s + - @repohash.sort.each do |repo, archlist| + - archlist.sort.each do |arch| + %td + = webui2_arch_repo_table_cell(repo, arch, packname, nil, false) +:javascript + setupProjectMonitor(); From 8f618abc062c7955c12d9613091b95b248e67300 Mon Sep 17 00:00:00 2001 From: Christian Bruckmayer Date: Fri, 30 Nov 2018 16:13:14 +0100 Subject: [PATCH 2/3] Revert f8981835987ee66beb7788897d902007cd1f234f MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The underlying issue got fixed in another way and this breaks other functionality of the modal. Co-authored-by: Ana María Martínez Gómez --- src/api/app/assets/stylesheets/webui2/modals.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/api/app/assets/stylesheets/webui2/modals.scss b/src/api/app/assets/stylesheets/webui2/modals.scss index 8d0c0bee2e6..3950ade5fc4 100644 --- a/src/api/app/assets/stylesheets/webui2/modals.scss +++ b/src/api/app/assets/stylesheets/webui2/modals.scss @@ -3,7 +3,3 @@ overflow-x: hidden; overflow-wrap: break-word; } - -.modal-body p { - white-space: pre-wrap; -} From 0386ddb2979bc67dbbd5de82c31bdb049479c497 Mon Sep 17 00:00:00 2001 From: Christian Bruckmayer Date: Tue, 4 Dec 2018 15:20:12 +0100 Subject: [PATCH 3/3] Add feature specs for bootstrap monitor page --- .../features/webui/projects/monitor_spec.rb | 101 ++++++++++++++++++ .../bootstrap/support/page/monitor_page.rb | 34 ++++++ src/api/spec/features/webui/projects_spec.rb | 4 + 3 files changed, 139 insertions(+) create mode 100644 src/api/spec/bootstrap/features/webui/projects/monitor_spec.rb create mode 100644 src/api/spec/bootstrap/support/page/monitor_page.rb diff --git a/src/api/spec/bootstrap/features/webui/projects/monitor_spec.rb b/src/api/spec/bootstrap/features/webui/projects/monitor_spec.rb new file mode 100644 index 00000000000..9aae9e9305b --- /dev/null +++ b/src/api/spec/bootstrap/features/webui/projects/monitor_spec.rb @@ -0,0 +1,101 @@ +require 'browser_helper' +require 'bootstrap/support/page/monitor_page' + +RSpec.feature 'Monitor', type: :feature, js: true do + describe 'monitor' do + let(:admin_user) { create(:admin_user) } + let!(:project) { create(:project, name: 'TestProject') } + let!(:package1) { create(:package, project: project, name: 'TestPackage') } + let!(:package2) { create(:package, project: project, name: 'SecondPackage') } + let!(:repository1) { create(:repository, project: project, name: 'openSUSE_Tumbleweed', architectures: ['x86_64', 'i586']) } + let!(:repository2) { create(:repository, project: project, name: 'openSUSE_Leap_42.3', architectures: ['x86_64', 'i586']) } + let!(:repository3) { create(:repository, project: project, name: 'openSUSE_Leap_42.2', architectures: ['x86_64', 'i586']) } + + let(:build_results_xml) do + <<-XML + + + +
no source uploaded
+
+ +
no source uploaded
+
+
+ + +
no source uploaded
+
+ +
no source uploaded
+
+
+ + +
no source uploaded
+
+ +
no source uploaded
+
+
+ + +
no source uploaded
+
+ +
no source uploaded
+
+
+ + +
no source uploaded
+
+ +
no source uploaded
+
+
+ + +
no source uploaded
+
+ +
no source uploaded
+
+
+
+ XML + end + + before do + login admin_user + allow(Backend::Api::BuildResults::Status).to receive(:result_swiss_knife).and_return(build_results_xml) + visit project_monitor_path(project.name) + end + + scenario 'filtering build results by architecture' do + page = Page::MonitorPage.new(:architectures) + page.filter('i586') + + expect(page).to have_column('i586') + expect(page).not_to have_column('x86_64') + end + + scenario 'filtering build results by repository' do + page = Page::MonitorPage.new(:repositories) + page.filter('openSUSE_Leap_42.2') + page.filter('openSUSE_Leap_42.3') + + expect(page).to have_column('openSUSE_Leap_42.2') + expect(page).to have_column('openSUSE_Leap_42.3') + expect(page).not_to have_column('Tumbleweed') + end + + scenario 'filtering build results by status' do + page = Page::MonitorPage.new(:status) + page.filter('succeeded') + + expect(page).to have_row('TestPackage') + expect(page).not_to have_row('SecondPackage') + end + end +end diff --git a/src/api/spec/bootstrap/support/page/monitor_page.rb b/src/api/spec/bootstrap/support/page/monitor_page.rb new file mode 100644 index 00000000000..0788cd0fa2b --- /dev/null +++ b/src/api/spec/bootstrap/support/page/monitor_page.rb @@ -0,0 +1,34 @@ +module Page + class MonitorPage + include Capybara::DSL + + def initialize(filter_name) + @filter_name = filter_name + end + + def filter(element) + find("#project-monitor-#{filter_name}-dropdown").click + find(:css, "label[for='#{element.parameterize}-checkbox']").click + end + + def has_column?(column) + header.has_text?(column) + end + + def has_row?(row) + rows.has_text?(row) + end + + private + + attr_reader :filter_name + + def rows + find('table#project-monitor-table') + end + + def header + find('.dataTables_scrollHead') + end + end +end diff --git a/src/api/spec/features/webui/projects_spec.rb b/src/api/spec/features/webui/projects_spec.rb index 365b3c2839f..bf28b098a5f 100644 --- a/src/api/spec/features/webui/projects_spec.rb +++ b/src/api/spec/features/webui/projects_spec.rb @@ -506,6 +506,7 @@ end scenario 'filtering build results by package name' do + skip_if_bootstrap # this is now handled by datatables, we don't need to test it fill_in 'pkgname', with: package1.name click_button 'Filter:' @@ -515,6 +516,7 @@ end scenario 'filtering build results by architecture' do + skip_if_bootstrap find('#archlink').click uncheck 'arch_x86_64' click_button 'Filter:' @@ -525,6 +527,7 @@ end scenario 'filtering build results by repository' do + skip_if_bootstrap find('#repolink').click uncheck 'repo_openSUSE_Leap_42_2' uncheck 'repo_openSUSE_Leap_42_3' @@ -537,6 +540,7 @@ end scenario 'filtering build results by last build' do + skip_if_bootstrap # we don't support this anymore check 'lastbuild' click_button 'Filter:'