From a0d0ffe53bff7cd849c978c12d686af0f82c2100 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 14 Jan 2017 15:27:50 +0000 Subject: [PATCH] added inline pipeline graph Added tests review changes --- app/assets/javascripts/dispatcher.js.es6 | 5 + .../javascripts/merge_request_widget.js.es6 | 20 +++- .../mini_pipeline_graph_dropdown.js.es6 | 2 - .../stylesheets/pages/merge_requests.scss | 43 ++++++++ app/assets/stylesheets/pages/pipelines.scss | 84 +++++++-------- .../projects/ci/pipelines/_pipeline.html.haml | 21 +--- .../merge_requests/widget/_heading.html.haml | 16 +-- .../shared/_mini_pipeline_graph.html.haml | 18 ++++ .../mini_pipeline_graph_spec.rb | 100 ++++++++++++++++++ .../mini_pipeline_graph_dropdown_spec.js.es6 | 4 +- 10 files changed, 238 insertions(+), 75 deletions(-) create mode 100644 app/views/shared/_mini_pipeline_graph.html.haml create mode 100644 spec/features/merge_requests/mini_pipeline_graph_spec.rb diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 048ceaa41f1..e1d8efe6848 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -159,6 +159,11 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commit:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }).bindEvents(); + break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 05b9a63765f..e5d2d706fc7 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -51,6 +51,8 @@ require('./smart_interval'); this.getCIStatus(false); this.retrieveSuccessIcon(); + this.initMiniPipelineGraph(); + this.ciStatusInterval = new global.SmartInterval({ callback: this.getCIStatus.bind(this, true), startingInterval: 10000, @@ -66,6 +68,7 @@ require('./smart_interval'); incrementByFactorOf: 15000, immediateExecution: true, }); + notifyPermissions(); } @@ -236,17 +239,20 @@ require('./smart_interval'); case "failed": case "canceled": case "not_found": - return this.setMergeButtonClass('btn-danger'); + this.setMergeButtonClass('btn-danger'); + break; case "running": - return this.setMergeButtonClass('btn-info'); + this.setMergeButtonClass('btn-info'); + break; case "success": case "success_with_warnings": - return this.setMergeButtonClass('btn-create'); + this.setMergeButtonClass('btn-create'); } } else { $('.ci_widget.ci-error').show(); - return this.setMergeButtonClass('btn-danger'); + this.setMergeButtonClass('btn-danger'); } + this.initMiniPipelineGraph(); }; MergeRequestWidget.prototype.showCICoverage = function(coverage) { @@ -269,6 +275,12 @@ require('./smart_interval'); $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); }; + MergeRequestWidget.prototype.initMiniPipelineGraph = function() { + new gl.MiniPipelineGraph({ + container: '.js-pipeline-inline-mr-widget-graph:visible', + }).bindEvents(); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 80549532ea9..4becbc32681 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -21,8 +21,6 @@ this.container = opts.container || ''; this.dropdownListSelector = '.js-builds-dropdown-container'; this.getBuildsList = this.getBuildsList.bind(this); - - this.bindEvents(); } /** diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0c013915a63..b01d8d695d6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -80,6 +80,10 @@ .ci_widget { border-bottom: 1px solid $well-inner-border; color: $gl-text-color; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; svg { margin-right: 4px; @@ -88,12 +92,20 @@ overflow: visible; } + &> span { + padding-right: 4px; + } + &.ci-success_with_warnings { i { color: $gl-warning; } } + + @media (max-width: $screen-xs-max) { + flex-wrap: wrap; + } } .mr-widget-body, @@ -102,6 +114,37 @@ padding: $gl-padding; } + .mr-widget-pipeline-graph { + flex-shrink: 0; + + .dropdown-menu { + margin-top: 11px; + } + + .ci-action-icon-wrapper { + line-height: 16px; + } + + @media (max-width: $screen-xs-max) { + order: 1; + margin-top: $gl-padding-top; + border-radius: 3px; + background-color: $white-light; + border: 1px solid $gray-darker; + width: 100%; + text-align: center; + + .dropdown-menu { + margin-left: -97.5px; + } + + .arrow-up::before, + .arrow-up::after, { + margin-left: 97.5px; + } + } + } + .normal { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 367a468e1ba..fdc9132a988 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -183,48 +183,6 @@ } } - .stage-cell { - font-size: 0; - padding: 10px 4px; - - > .stage-container > div > button > span > svg, - > .stage-container > button > svg { - height: 22px; - width: 22px; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; - } - - .stage-container { - display: inline-block; - position: relative; - height: 22px; - margin: 3px 6px 3px 0; - - .tooltip { - white-space: nowrap; - } - - .tooltip-inner { - padding: 3px 4px; - } - - &:not(:last-child) { - &::after { - content: ''; - width: 7px; - position: absolute; - right: -7px; - top: 10px; - border-bottom: 2px solid $border-color; - } - } - } - } - .duration, .finished-at { color: $gl-text-color-secondary; @@ -311,6 +269,48 @@ } } +.stage-cell { + font-size: 0; + padding: 10px 4px; + + > .stage-container > div > button > span > svg, + > .stage-container > button > svg { + height: 22px; + width: 22px; + position: absolute; + top: -1px; + left: -1px; + z-index: 2; + overflow: visible; + } + + .stage-container { + display: inline-block; + position: relative; + height: 22px; + margin: 3px 6px 3px 0; + + .tooltip { + white-space: nowrap; + } + + .tooltip-inner { + padding: 3px 4px; + } + + &:not(:last-child) { + &::after { + content: ''; + width: 7px; + position: absolute; + right: -7px; + top: 10px; + border-bottom: 2px solid $border-color; + } + } + } +} + .admin-builds-table { .ci-table td:last-child { min-width: 120px; diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index cdab1e1b1a6..ac0fd87fd8d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -40,25 +40,8 @@ - else Cant find HEAD commit for this branch - %td.stage-cell - - pipeline.stages.each do |stage| - - if stage.status - - detailed_status = stage.detailed_status(current_user) - - icon_status = "#{detailed_status.icon}_borderless" - - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - - .stage-container.dropdown.js-mini-pipeline-graph - %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } - = custom_icon(icon_status) - = icon('caret-down') - - %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container - .arrow-up - .js-builds-dropdown-list.scrollable-menu - - .js-builds-dropdown-loading.builds-dropdown-loading.hidden - %span.fa.fa-spinner.fa-spin - + %td + = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph' %td - if pipeline.duration diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ae134563ead..bef76f16ca7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,16 +1,20 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do - = ci_icon_for_status(status) + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + %div{ class: "ci-status-icon-#{status}" } + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do + = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) - for - = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" + .mr-widget-pipeline-graph + = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' + %span + for + = succeed "." do + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml new file mode 100644 index 00000000000..b0778653d4e --- /dev/null +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -0,0 +1,18 @@ +.stage-cell + - pipeline.stages.each do |stage| + - if stage.status + - detailed_status = stage.detailed_status(current_user) + - icon_status = "#{detailed_status.icon}_borderless" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" + + .stage-container.dropdown{ class: klass } + %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } + = custom_icon(icon_status) + = icon('caret-down') + + %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container + .arrow-up + .js-builds-dropdown-list.scrollable-menu + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb new file mode 100644 index 00000000000..b08bd36bde9 --- /dev/null +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -0,0 +1,100 @@ +require 'rails_helper' + +feature 'Mini Pipeline Graph', :js, :feature do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + + let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } + let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + + before do + build.run + + login_as(user) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should display a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') + end + + describe 'build list toggle' do + let(:toggle) do + find('.mini-pipeline-graph-dropdown-toggle') + first('.mini-pipeline-graph-dropdown-toggle') + end + + it 'should expand when hovered' do + before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + + toggle.hover + + after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + + expect(before_width).to be < after_width + end + + it 'should show dropdown caret when hovered' do + toggle.hover + + expect(toggle).to have_selector('.fa-caret-down') + end + + it 'should show tooltip when hovered' do + toggle.hover + + expect(toggle.find(:xpath, '..')).to have_selector('.tooltip') + end + end + + describe 'builds list menu' do + let(:toggle) do + find('.mini-pipeline-graph-dropdown-toggle') + first('.mini-pipeline-graph-dropdown-toggle') + end + + before do + toggle.click + wait_for_ajax + end + + it 'should open when toggle is clicked' do + expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + it 'should close when toggle is clicked again' do + toggle.click + + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + it 'should close when clicking somewhere else' do + find('body').click + + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + describe 'build list build item' do + let(:build_item) do + find('.mini-pipeline-graph-dropdown-item') + first('.mini-pipeline-graph-dropdown-item') + end + + it 'should visit the build page when clicked' do + build_item.click + find('.build-page') + + expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build)) + end + + it 'should show tooltip when hovered' do + build_item.hover + + expect(build_item.find(:xpath, '..')).to have_selector('.tooltip') + end + end + end +end diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index a6994f6edf4..7cdade01e00 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -31,7 +31,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should call getBuildsList', () => { const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); @@ -41,7 +41,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should make a request to the endpoint provided in the html', () => { const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');