From a0d0ffe53bff7cd849c978c12d686af0f82c2100 Mon Sep 17 00:00:00 2001
From: "Luke \"Jared\" Bennett" <lbennett@gitlab.com>
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');
-- 
GitLab