summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzegorz@gitlab.com>2016-12-20 21:21:52 +0000
committerGrzegorz Bizon <grzegorz@gitlab.com>2016-12-20 21:21:52 +0000
commit92a91a880327877fe3ac1825a4c5b638333cf586 (patch)
tree5d892cda3cd9c43af94d9109345cddbf3ee27c9d
parentd814533f690b4dba7d32006a97f2f696161fa564 (diff)
parent2b486c2bb27087e4eb306821b9fca95ff8ac74d3 (diff)
downloadgitlab-ce-92a91a880327877fe3ac1825a4c5b638333cf586.tar.gz
Merge branch '19703-direct-link-pipelines' into 'master'
Resolve "Direct link from pipeline list to builds" ## What does this MR do? - Adds a dropdown with builds in the mini pipeline graph in the pipelines table - Unnest a lot of CSS related with pipelines in order to make it reusable ## Screenshots ![Screen_Shot_2016-12-15_at_14.45.41](/uploads/ca1c61842a422a34383e029d668034b7/Screen_Shot_2016-12-15_at_14.45.41.png) ![Screen_Shot_2016-12-15_at_14.45.49](/uploads/952e3277143639ce4ad111103034faeb/Screen_Shot_2016-12-15_at_14.45.49.png) ![Screen_Shot_2016-12-15_at_14.46.02](/uploads/f7369a124b1c3c0db4194de2cb637ef0/Screen_Shot_2016-12-15_at_14.46.02.png) ![graph_animation](/uploads/9bae036cb5acff499f992a4722943d72/graph_animation.gif) ## Does this MR meet the acceptance criteria? - [x] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [x] Added for this feature/bug - [ ] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #25071 Closes #19703 See merge request !8097
-rw-r--r--app/assets/javascripts/dispatcher.js.es615
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es696
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss386
-rw-r--r--app/controllers/projects/pipelines_controller.rb10
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/stage.rb4
-rw-r--r--app/views/ci/status/_graph_badge.html.haml12
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml27
-rw-r--r--app/views/projects/commit/_pipelines_list.haml16
-rw-r--r--app/views/projects/pipelines/_stage.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml14
-rw-r--r--app/views/shared/icons/_icon_status_canceled_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_created_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_failed_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_manual_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_pending_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_running_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_skipped_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_success_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_status_warning_borderless.svg1
-rw-r--r--changelogs/unreleased/19703-direct-link-pipelines.yml4
-rw-r--r--config/routes/project.rb1
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb47
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb72
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml8
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es651
-rw-r--r--spec/models/ci/pipeline_spec.rb24
-rw-r--r--spec/models/ci/stage_spec.rb13
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb21
30 files changed, 684 insertions, 157 deletions
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 1e259a16f06..752f35e6356 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -141,6 +141,11 @@
case 'projects:merge_requests:builds':
new MergedButtons();
break;
+ case 'projects:merge_requests:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
@@ -158,6 +163,11 @@
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:commit:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ break;
case 'projects:commit:builds':
new gl.Pipelines();
break;
@@ -172,6 +182,11 @@
new TreeView();
}
break;
+ case 'projects:pipelines:index':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
new file mode 100644
index 00000000000..90b3366f14b
--- /dev/null
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -0,0 +1,96 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+/**
+ * In each pipelines table we have a mini pipeline graph for each pipeline.
+ *
+ * When we click in a pipeline stage, we need to make an API call to get the
+ * builds list to render in a dropdown.
+ *
+ * The container should be the table element.
+ *
+ * The stage icon clicked needs to have the following HTML structure:
+ * <div>
+ * <button class="dropdown js-builds-dropdown-button"></button>
+ * <div class="js-builds-dropdown-container"></div>
+ * </div>
+ */
+(() => {
+ class MiniPipelineGraph {
+ constructor(opts = {}) {
+ this.container = opts.container || '';
+ this.dropdownListSelector = '.js-builds-dropdown-container';
+ this.getBuildsList = this.getBuildsList.bind(this);
+
+ this.bindEvents();
+ }
+
+ /**
+ * Adds and removes the event listener.
+ */
+ bindEvents() {
+ const dropdownButtonSelector = 'button.js-builds-dropdown-button';
+
+ $(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
+ .on('click', dropdownButtonSelector, this.getBuildsList);
+ }
+
+ /**
+ * For the clicked stage, renders the given data in the dropdown list.
+ *
+ * @param {HTMLElement} stageContainer
+ * @param {Object} data
+ */
+ renderBuildsList(stageContainer, data) {
+ const dropdownContainer = stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ );
+
+ dropdownContainer.innerHTML = data;
+ }
+
+ /**
+ * For the clicked stage, gets the list of builds.
+ *
+ * @param {Object} e
+ * @return {Promise}
+ */
+ getBuildsList(e) {
+ const button = e.currentTarget;
+ const endpoint = button.dataset.stageEndpoint;
+
+ return $.ajax({
+ dataType: 'json',
+ type: 'GET',
+ url: endpoint,
+ beforeSend: () => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+ },
+ success: (data) => {
+ this.toggleLoading(button);
+ this.renderBuildsList(button, data.html);
+ },
+ error: () => {
+ this.toggleLoading(button);
+ new Flash('An error occurred while fetching the builds.', 'alert');
+ },
+ });
+ }
+
+ /**
+ * Toggles the visibility of the loading icon.
+ *
+ * @param {HTMLElement} stageContainer
+ * @return {type}
+ */
+ toggleLoading(stageContainer) {
+ stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+ ).classList.toggle('hidden');
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MiniPipelineGraph = MiniPipelineGraph;
+})();
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index c9d54b4f3d3..621b780ce4d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -22,17 +22,22 @@
.table.ci-table {
min-width: 1200px;
+ table-layout: fixed;
.pipeline-id {
color: $black;
}
- .branch-commit {
- width: 30%;
+ .pipeline-date,
+ .pipeline-status {
+ width: 10%;
+ }
- .branch-name {
- max-width: 195px;
- }
+ .pipeline-info,
+ .pipeline-commit,
+ .pipeline-actions,
+ .pipeline-stages {
+ width: 20%;
}
}
}
@@ -106,7 +111,7 @@
.branch-name {
font-weight: bold;
- max-width: 150px;
+ max-width: 120px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
@@ -132,7 +137,7 @@
.commit-title {
margin-top: 4px;
- max-width: 300px;
+ max-width: 225px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -192,10 +197,6 @@
border-bottom: 2px solid $border-color;
}
}
-
- a {
- display: block;
- }
}
}
@@ -462,6 +463,25 @@
white-space: normal;
color: $gl-text-color-light;
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-light;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+
+ .dropdown-counter-badge {
+ color: $gl-text-color;
+ }
+ }
+ }
+
> .build-content {
display: inline-block;
padding: 8px 10px 9px;
@@ -527,7 +547,7 @@
content: '';
position: absolute;
top: 48%;
- right: -49px;
+ right: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
@@ -574,156 +594,280 @@
}
}
}
+}
- .ci-status-text {
- max-width: 110px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: bottom;
+.dropdown-counter-badge {
+ float: right;
+ color: $border-color;
+ font-weight: 100;
+ font-size: 15px;
+ margin-right: 2px;
+}
+
+.grouped-pipeline-dropdown {
+ padding: 0;
+ width: 191px;
+ left: auto;
+ right: -195px;
+ top: -4px;
+ box-shadow: 0 1px 5px $black-transparent;
+
+ a {
display: inline-block;
- position: relative;
- font-weight: 100;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ }
}
- .dropdown-menu-toggle {
- background-color: transparent;
- border: none;
- padding: 0;
- color: $gl-text-color-light;
- white-space: normal;
- overflow: visible;
+ ul {
+ max-height: 245px;
+ overflow: auto;
+ margin: 5px 0;
+
+ li {
+ padding-top: 2px;
+ margin: 0 5px;
+ padding-left: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
+ line-height: 1.2;
+ }
+ }
+}
+
+.ci-status-text {
+ max-width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ display: inline-block;
+ position: relative;
+ font-weight: 100;
+}
+
+// Action Icons
+.ci-action-icon-container .ci-action-icon-wrapper {
+ i {
+ color: $border-color;
+ border-radius: 100%;
+ border: 1px solid $border-color;
+ padding: 5px 6px;
+ font-size: 13px;
+ background: $white-light;
+ height: 30px;
+ width: 30px;
- &:focus {
- outline: none;
+ &::before {
+ position: relative;
+ top: 3px;
+ left: 3px;
}
&:hover {
color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
+ }
+ }
- .dropdown-counter-badge {
- color: $gl-text-color;
+ .ci-play-icon {
+ padding: 5px 5px 5px 7px;
+ }
+}
+
+.dropdown-build {
+ color: $gl-text-color-light;
+
+ .ci-action-icon-container {
+ padding: 0;
+ font-size: 11px;
+ float: right;
+ margin-top: 4px;
+ display: inline-block;
+ position: relative;
+
+ i {
+ font-size: 11px;
+ margin-top: 0;
+ }
+ }
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border-radius: 3px;
+ color: $gl-text-color;
+ }
+
+ .ci-action-icon-container {
+ i {
+ width: 25px;
+ height: 25px;
+
+ &::before {
+ top: 1px;
+ left: 1px;
}
}
}
- .dropdown-counter-badge {
- float: right;
- clear: right;
- color: $border-color;
- font-weight: 100;
- font-size: 15px;
- margin-right: 2px;
+ .stage {
+ max-width: 100px;
+ width: 100px;
}
- .grouped-pipeline-dropdown {
+ .ci-status-icon svg {
+ height: 18px;
+ width: 18px;
+ }
+
+ .ci-status-text {
+ max-width: 95px;
+ }
+}
+
+/**
+ * Builds dropdown in mini pipeline
+ */
+.mini-pipeline-graph {
+ .builds-dropdown {
+ background-color: transparent;
+ border: none;
padding: 0;
- width: 191px;
- left: auto;
- right: -195px;
- top: -4px;
- box-shadow: 0 1px 5px $black-transparent;
+ color: $gl-text-color-light;
+ border: none;
+ margin: 0;
+ }
+
+ .builds-dropdown-loading {
+ margin: 10px auto;
+ width: 18px;
+ }
+
+ .grouped-pipeline-dropdown {
+ right: -172px;
+ top: 23px;
+ min-height: 50px;
a {
- display: inline-block;
+ color: $gl-text-color-light;
+ }
+ }
- &:hover {
- background-color: $stage-hover-bg;
- }
+ .arrow-up {
+ &::before,
+ &::after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: -6px;
+ left: 2px;
+ border-width: 0 5px 6px;
}
- ul {
- max-height: 245px;
- overflow: auto;
- margin: 5px 0;
-
- li {
- margin: 0 5px;
- padding-left: 0;
- padding-bottom: 0;
- margin-bottom: 0;
- line-height: 1.2;
- }
+ &::before {
+ border-width: 0 5px 5px;
+ border-bottom-color: $border-color;
}
- .dropdown-build {
- color: $gl-text-color-light;
+ &::after {
+ margin-top: 1px;
+ border-bottom-color: $white-light;
+ }
+ }
+}
- .build-content {
- width: 100%;
- }
+/**
+ * Icons in mini pipeline graph
+ */
+.mini-pipeline-graph-icon-container .ci-status-icon {
+ display: inline-block;
+ border: 1px solid;
+ border-radius: 20px;
+ margin-right: 1px;
+ width: 20px;
+ height: 20px;
+ position: relative;
+ z-index: 2;
+ transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
- .ci-action-icon-container {
- font-size: 11px;
- position: absolute;
- right: 4px;
+ svg {
+ top: -1px;
+ }
+}
- i {
- width: 25px;
- height: 25px;
- font-size: 11px;
- margin-top: 0;
+.builds-dropdown {
+ &:focus {
+ outline: none;
+ margin-right: -8px;
- &::before {
- top: 1px;
- left: 1px;
- }
- }
- }
+ .ci-status-icon {
+ width: 28px;
+ padding: 0 8px 0 0;
+ transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
- &:hover {
- background-color: $stage-hover-bg;
- border-radius: 3px;
- color: $gl-text-color;
+ + .dropdown-caret {
+ display: inline-block;
}
+ }
+ }
- .stage {
- max-width: 100px;
- width: 100px;
- }
+ &:focus,
+ &:active {
+ .ci-status-icon-success {
+ background-color: rgba($gl-success, .1);
+ }
- .ci-status-icon svg {
- height: 18px;
- width: 18px;
- }
+ .ci-status-icon-failed {
+ background-color: rgba($gl-danger, .1);
+ }
- .ci-status-text {
- max-width: 95px;
- padding-bottom: 3px;
- position: relative;
- top: 3px;
- }
+ .ci-status-icon-pending,
+ .ci-status-icon-success_with_warnings {
+ background-color: rgba($gl-warning, .1);
}
- }
-}
-// Action Icons
-.ci-action-icon-container .ci-action-icon-wrapper {
- i {
- color: $border-color;
- border-radius: 100%;
- border: 1px solid $border-color;
- padding: 5px 6px;
- font-size: 13px;
- background: $white-light;
- height: 30px;
- width: 30px;
+ .ci-status-icon-running {
+ background-color: rgba($blue-normal, .1);
+ }
- &::before {
- position: relative;
- top: 3px;
- left: 3px;
+ .ci-status-icon-canceled,
+ .ci-status-icon-disabled,
+ .ci-status-icon-not-found {
+ background-color: rgba($gl-gray, .1);
}
- &:hover {
- color: $gl-text-color;
- background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-bg;
+ .ci-status-icon-created,
+ .ci-status-icon-skipped {
+ background-color: rgba($gray-darkest, .1);
}
}
- .ci-play-icon {
- padding: 5px 5px 5px 7px;
+ .mini-pipeline-graph-icon-container {
+ .ci-status-icon:hover,
+ .ci-status-icon:focus {
+ width: 28px;
+ padding: 0 8px 0 0;
+
+ + .dropdown-caret {
+ display: inline-block;
+ }
+ }
+
+ .dropdown-caret {
+ font-size: 11px;
+ position: relative;
+ top: 3px;
+ left: -11px;
+ margin-right: -6px;
+ display: none;
+ z-index: 2;
+ }
}
}
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 85188cfdd4c..cc347922c6a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+ @pipelines = @pipelines.includes(project: :namespace)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count
@@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def stage
+ @stage = pipeline.stage(params[:stage])
+ return not_found unless @stage
+
+ respond_to do |format|
+ format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
+ end
+ end
+
def retry
pipeline.retry_failed(current_user)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 48354cdbefb..f2f6453b3b9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -116,6 +116,11 @@ module Ci
where.not(duration: nil).sum(:duration)
end
+ def stage(name)
+ stage = Ci::Stage.new(self, name: name)
+ stage unless stage.statuses_count.zero?
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 7ef59445d77..d035eda6df5 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -18,6 +18,10 @@ module Ci
name
end
+ def statuses_count
+ @statuses_count ||= statuses.count
+ end
+
def status
@status ||= statuses.latest.status
end
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index 52b4d77d074..dd2f649de9a 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -3,18 +3,18 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
- = link_to status.details_path, class: 'build-content' do
+ = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do
%span{ class: klass }= custom_icon(status.icon)
- .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
+ .ci-status-text= subject.name
- else
- .build-content
+ .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
%span{ class: klass }= custom_icon(status.icon)
- .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
+ .ci-status-text= subject.name
- if status.has_action?
- = link_to status.action_path, method: status.action_method,
- title: status.action_title, class: 'ci-action-icon-container' do
+ = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
%i.ci-action-icon-wrapper
= icon(status.action_icon, class: status.action_class)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 3f05a21990f..2f8f153f9a9 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -43,10 +43,25 @@
%td.stage-cell
- pipeline.stages.each do |stage|
- if stage.status
- - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}"
- .stage-container
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do
- = ci_icon_for_status(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.mini-pipeline-graph
+ .dropdown.inline.build-content
+ %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ 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)}}
+ %span.has-tooltip{ class: status_klass }
+ %span.mini-pipeline-graph-icon-container
+ %span{ class: status_klass }= custom_icon(icon_status)
+ = icon('caret-down', class: 'dropdown-caret')
+
+ .js-builds-dropdown-container
+ .dropdown-menu.grouped-pipeline-dropdown
+ .arrow-up
+ .js-builds-dropdown-list
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
%td
- if pipeline.duration
@@ -66,7 +81,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -77,7 +92,7 @@
%span= build.name.humanize
- if artifacts.present?
.btn-group
- %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 7f42fde0fea..5a9f7295135 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -4,12 +4,12 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.ci-table
- %tbody
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th
+ %table.table.ci-table.js-pipeline-table
+ %thead
+ %th.pipeline-status Status
+ %th.pipeline-info Pipeline
+ %th.pipeline-commit Commit
+ %th.pipeline-stages Stages
+ %th.pipeline-date
+ %th.pipeline-actions
= render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
new file mode 100644
index 00000000000..20456e792e7
--- /dev/null
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -0,0 +1,4 @@
+%ul
+ - @stage.statuses.each do |status|
+ %li.dropdown-build
+ = render 'ci/status/graph_badge', subject: status
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 030cd8ef78f..28026ccf861 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -42,14 +42,14 @@
.nothing-here-block No pipelines to show
- else
.table-holder
- %table.table.ci-table
+ %table.table.ci-table.js-pipeline-table
%thead
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th.hidden-xs
+ %th.pipeline-status Status
+ %th.pipeline-info Pipeline
+ %th.pipeline-commit Commit
+ %th.pipeline-stages Stages
+ %th.pipeline-date
+ %th.pipeline-actions.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg
new file mode 100644
index 00000000000..bf7fb29185f
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg
new file mode 100644
index 00000000000..1810d023be8
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg>
diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg
new file mode 100644
index 00000000000..b7022350c74
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_failed_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg
new file mode 100644
index 00000000000..5eec665688b
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg>
diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg
new file mode 100644
index 00000000000..8d66e9e6c9c
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_pending_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg
new file mode 100644
index 00000000000..2757a168ed5
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_running_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg
new file mode 100644
index 00000000000..fb3e930b3cb
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg
new file mode 100644
index 00000000000..8ee5be7ab78
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_success_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg
new file mode 100644
index 00000000000..7b061624521
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_warning_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg>
diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml
new file mode 100644
index 00000000000..d846ad41e0f
--- /dev/null
+++ b/changelogs/unreleased/19703-direct-link-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Adds Direct link from pipeline list to builds
+merge_request: 8097
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 80cc47ab9a8..335fccb617b 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -139,6 +139,7 @@ constraints(ProjectUrlConstrainer.new) do
end
member do
+ get :stage
post :cancel
post :retry
get :builds
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
new file mode 100644
index 00000000000..5fe7e6407cc
--- /dev/null
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Projects::PipelinesController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET stages.json' do
+ context 'when accessing existing stage' do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'build')
+
+ get_stage('build')
+ end
+
+ it 'returns html source for stage dropdown' do
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template('projects/pipelines/_stage')
+ expect(json_response).to include('html')
+ end
+ end
+
+ context 'when accessing unknown stage' do
+ before do
+ get_stage('test')
+ end
+
+ it 'responds with not found' do
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ def get_stage(name)
+ get :stage, namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: pipeline.id,
+ stage: name,
+ format: :json
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 1210e2745db..14e009daba8 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Pipelines", feature: true, js: true do
+describe 'Pipeline', :feature, :js do
include GitlabRoutingHelper
let(:project) { create(:empty_project) }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f3731698a18..1ff57f92c4c 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
-describe "Pipelines" do
+describe 'Pipelines', :feature, :js do
include GitlabRoutingHelper
+ include WaitForAjax
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -69,16 +70,32 @@ describe "Pipelines" do
end
context 'with manual actions' do
- let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
+ let!(:manual) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'manual build',
+ stage: 'test',
+ commands: 'test')
+ end
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
- it { expect(page).to have_link('Manual build') }
+ it 'has link to the manual action' do
+ find('.js-pipeline-dropdown-manual-actions').click
- context 'when playing' do
- before { click_link('Manual build') }
+ expect(page).to have_link('Manual build')
+ end
- it { expect(manual.reload).to be_pending }
+ context 'when manual action was played' do
+ before do
+ find('.js-pipeline-dropdown-manual-actions').click
+ click_link('Manual build')
+ end
+
+ it 'enqueues manual action job' do
+ expect(manual.reload).to be_pending
+ end
end
end
@@ -131,7 +148,10 @@ describe "Pipelines" do
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') }
- it { expect(page).to have_link(with_artifacts.name) }
+ it do
+ find('.js-pipeline-dropdown-download').click
+ expect(page).to have_link(with_artifacts.name)
+ end
end
context 'with artifacts expired' do
@@ -150,6 +170,42 @@ describe "Pipelines" do
it { expect(page).not_to have_selector('.build-artifacts') }
end
end
+
+ context 'mini pipleine graph' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build')
+ end
+
+ before do
+ visit namespace_project_pipelines_path(project.namespace, project)
+ end
+
+ it 'should render a mini pipeline graph' do
+ endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name)
+
+ expect(page).to have_selector('.mini-pipeline-graph')
+ expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']")
+ end
+
+ context 'when clicking a graph stage' do
+ it 'should open a dropdown' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ wait_for_ajax
+
+ expect(page).to have_link build.name
+ end
+
+ it 'should be possible to retry the failed build' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ wait_for_ajax
+
+ find('a.ci-action-icon-container').trigger('click')
+ expect(page).not_to have_content('Cancel running')
+ end
+ end
+ end
end
describe 'POST /:project/pipelines' do
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
new file mode 100644
index 00000000000..e9bf7568e95
--- /dev/null
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -0,0 +1,8 @@
+%div.js-builds-dropdown-tests
+ %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'}
+ Dropdown
+ %div.js-builds-dropdown-container
+ %div.js-builds-dropdown-list
+
+ %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
new file mode 100644
index 00000000000..d1793e9308e
--- /dev/null
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
@@ -0,0 +1,51 @@
+/* eslint-disable no-new */
+
+//= require flash
+//= require mini_pipeline_graph_dropdown
+
+(() => {
+ describe('Mini Pipeline Graph Dropdown', () => {
+ fixture.preload('mini_dropdown_graph');
+
+ beforeEach(() => {
+ fixture.load('mini_dropdown_graph');
+ });
+
+ describe('When is initialized', () => {
+ it('should initialize without errors when no options are given', () => {
+ const miniPipelineGraph = new window.gl.MiniPipelineGraph();
+
+ expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
+ });
+
+ it('should set the container as the given prop', () => {
+ const container = '.foo';
+
+ const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container });
+
+ expect(miniPipelineGraph.container).toEqual(container);
+ });
+ });
+
+ describe('When dropdown is clicked', () => {
+ it('should call getBuildsList', () => {
+ const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
+
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+
+ document.querySelector('.js-builds-dropdown-button').click();
+
+ expect(getBuildsListSpy).toHaveBeenCalled();
+ });
+
+ 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' });
+
+ document.querySelector('.js-builds-dropdown-button').click();
+ expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ });
+ });
+ });
+})();
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 52dd41065e9..dc377d15f15 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#stage' do
+ subject { pipeline.stage('test') }
+
+ context 'with status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'test')
+ end
+
+ it { expect(subject).to be_a Ci::Stage }
+ it { expect(subject.name).to eq 'test' }
+ it { expect(subject.statuses).not_to be_empty }
+ end
+
+ context 'without status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build')
+ end
+
+ it 'return stage object' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
let(:build) { create_build('build1', 0) }
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 8fff38f7cda..742bedb37e4 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -28,6 +28,19 @@ describe Ci::Stage, models: true do
end
end
+ describe '#statuses_count' do
+ before do
+ create_job(:ci_build)
+ create_job(:ci_build, stage: 'other stage')
+ end
+
+ subject { stage.statuses_count }
+
+ it "counts statuses only from current stage" do
+ is_expected.to eq(1)
+ end
+ end
+
describe '#builds' do
let!(:stage_build) { create_job(:ci_build) }
let!(:commit_status) { create_job(:commit_status) }
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
new file mode 100644
index 00000000000..eb7f7ca4a1a
--- /dev/null
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/_stage', :view do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:stage) { build(:ci_stage, pipeline: pipeline) }
+
+ before do
+ assign :stage, stage
+
+ create(:ci_build, name: 'test:build',
+ stage: stage.name,
+ pipeline: pipeline)
+ end
+
+ it 'shows the builds in the stage' do
+ render
+
+ expect(rendered).to have_text 'test:build'
+ end
+end