Commit 92a91a88 authored by Grzegorz Bizon's avatar Grzegorz Bizon

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
parents d814533f 2b486c2b
...@@ -141,6 +141,11 @@ ...@@ -141,6 +141,11 @@
case 'projects:merge_requests:builds': case 'projects:merge_requests:builds':
new MergedButtons(); new MergedButtons();
break; break;
case 'projects:merge_requests:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case "projects:merge_requests:diffs": case "projects:merge_requests:diffs":
new gl.Diff(); new gl.Diff();
new ZenMode(); new ZenMode();
...@@ -158,6 +163,11 @@ ...@@ -158,6 +163,11 @@
new ZenMode(); new ZenMode();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:commit:builds': case 'projects:commit:builds':
new gl.Pipelines(); new gl.Pipelines();
break; break;
...@@ -172,6 +182,11 @@ ...@@ -172,6 +182,11 @@
new TreeView(); new TreeView();
} }
break; break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:show': case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
......
/* 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;
})();
...@@ -22,17 +22,22 @@ ...@@ -22,17 +22,22 @@
.table.ci-table { .table.ci-table {
min-width: 1200px; min-width: 1200px;
table-layout: fixed;
.pipeline-id { .pipeline-id {
color: $black; color: $black;
} }
.branch-commit { .pipeline-date,
width: 30%; .pipeline-status {
width: 10%;
}
.branch-name { .pipeline-info,
max-width: 195px; .pipeline-commit,
} .pipeline-actions,
.pipeline-stages {
width: 20%;
} }
} }
} }
...@@ -106,7 +111,7 @@ ...@@ -106,7 +111,7 @@
.branch-name { .branch-name {
font-weight: bold; font-weight: bold;
max-width: 150px; max-width: 120px;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
...@@ -132,7 +137,7 @@ ...@@ -132,7 +137,7 @@
.commit-title { .commit-title {
margin-top: 4px; margin-top: 4px;
max-width: 300px; max-width: 225px;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -192,10 +197,6 @@ ...@@ -192,10 +197,6 @@
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
a {
display: block;
}
} }
} }
...@@ -462,6 +463,25 @@ ...@@ -462,6 +463,25 @@
white-space: normal; white-space: normal;
color: $gl-text-color-light; 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 { > .build-content {
display: inline-block; display: inline-block;
padding: 8px 10px 9px; padding: 8px 10px 9px;
...@@ -527,7 +547,7 @@ ...@@ -527,7 +547,7 @@
content: ''; content: '';
position: absolute; position: absolute;
top: 48%; top: 48%;
right: -49px; right: -48px;
border-top: 2px solid $border-color; border-top: 2px solid $border-color;
width: 48px; width: 48px;
height: 1px; height: 1px;
...@@ -574,156 +594,280 @@ ...@@ -574,156 +594,280 @@
} }
} }
} }
}
.ci-status-text { .dropdown-counter-badge {
max-width: 110px; float: right;
white-space: nowrap; color: $border-color;
overflow: hidden; font-weight: 100;
text-overflow: ellipsis; font-size: 15px;
vertical-align: bottom; 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; display: inline-block;
position: relative;
font-weight: 100; &:hover {
background-color: $stage-hover-bg;
}
} }
.dropdown-menu-toggle { ul {
background-color: transparent; max-height: 245px;
border: none; overflow: auto;
padding: 0; margin: 5px 0;
color: $gl-text-color-light;
white-space: normal; li {
overflow: visible; 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 { &::before {
outline: none; position: relative;
top: 3px;
left: 3px;
} }
&:hover { &:hover {
color: $gl-text-color; color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-bg;
}
}
.dropdown-counter-badge { .ci-play-icon {
color: $gl-text-color; 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 { .stage {
float: right; max-width: 100px;
clear: right; width: 100px;
color: $border-color;
font-weight: 100;
font-size: 15px;
margin-right: 2px;
} }
.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; padding: 0;
width: 191px; color: $gl-text-color-light;
left: auto; border: none;
right: -195px; margin: 0;
top: -4px; }
box-shadow: 0 1px 5px $black-transparent;
.builds-dropdown-loading {
margin: 10px auto;
width: 18px;
}
.grouped-pipeline-dropdown {
right: -172px;
top: 23px;
min-height: 50px;
a { a {
display: inline-block; color: $gl-text-color-light;
}
}
&:hover { .arrow-up {
background-color: $stage-hover-bg; &::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 { &::before {
max-height: 245px; border-width: 0 5px 5px;
overflow: auto; border-bottom-color: $border-color;
margin: 5px 0;
li {
margin: 0 5px;
padding-left: 0;
padding-bottom: 0;
margin-bottom: 0;
line-height: 1.2;
}
} }
.dropdown-build { &::after {
color: $gl-text-color-light; 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 { svg {
font-size: 11px; top: -1px;
position: absolute; }
right: 4px; }
i { .builds-dropdown {
width: 25px; &:focus {
height: 25px; outline: none;
font-size: 11px; margin-right: -8px;
margin-top: 0;
&::before { .ci-status-icon {
top: 1px; width: 28px;
left: 1px; padding: 0 8px 0 0;
} transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
}
}
&:hover { + .dropdown-caret {
background-color: $stage-hover-bg; display: inline-block;
border-radius: 3px;
color: $gl-text-color;
} }
}
}
.stage { &:focus,
max-width: 100px; &:active {
width: 100px; .ci-status-icon-success {
} background-color: rgba($gl-success, .1);
}
.ci-status-icon svg { .ci-status-icon-failed {
height: 18px; background-color: rgba($gl-danger, .1);
width: 18px; }
}
.ci-status-text { .ci-status-icon-pending,
max-width: 95px; .ci-status-icon-success_with_warnings {
padding-bottom: 3px; background-color: rgba($gl-warning, .1);
position: relative;
top: 3px;
}
} }
}
}
// Action Icons .ci-status-icon-running {
.ci-action-icon-container .ci-action-icon-wrapper { background-color: rgba($blue-normal, .1);
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;
&::before { .ci-status-icon-canceled,
position: relative; .ci-status-icon-disabled,
top: 3px; .ci-status-icon-not-found {
left: 3px; background-color: rgba($gl-gray, .1);
} }
&:hover { .ci-status-icon-created,
color: $gl-text-color; .ci-status-icon-skipped {
background-color: $stage-hover-bg; background-color: rgba($gray-darkest, .1);
border: 1px solid $stage-hover-bg;
} }
} }
.ci-play-icon { .mini-pipeline-graph-icon-container {
padding: 5px 5px 5px 7px; .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;
}
} }
} }
......
...@@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) @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 @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count @pipelines_count = PipelinesFinder.new(project).execute.count
...@@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
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 def retry
pipeline.retry_failed(current_user) pipeline.retry_failed(current_user)
......
...@@ -116,6 +116,11 @@ module Ci ...@@ -116,6 +116,11 @@ module Ci
where.not(duration: nil).sum(:duration) where.not(duration: nil).sum(:duration)
end end
def stage(name)
stage = Ci::Stage.new(self, name: name)
stage unless stage.statuses_count.zero?
end
def stages_count def stages_count
statuses.select(:stage).distinct.count statuses.select(:stage).distinct.count
end end
......
...@@ -18,6 +18,10 @@ module Ci ...@@ -18,6 +18,10 @@ module Ci
name name
end end
def statuses_count
@statuses_count ||= statuses.count
end
def status def status
@status ||= statuses.latest.status @status ||= statuses.latest.status
end end
......
...@@ -3,18 +3,18 @@ ...@@ -3,18 +3,18 @@
- subject = local_assigns.fetch(:subject) - subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user) - status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}" - klass = "ci-status-icon ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details? - 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) %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 - else
.build-content .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
%span{ class: klass }= custom_icon(status.icon) %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? - if status.has_action?
= link_to status.action_path, method: status.action_method, = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
title: status.action_title, class: 'ci-action-icon-container' do
%i.ci-action-icon-wrapper %i.ci-action-icon-wrapper
= icon(status.action_icon, class: status.action_class) = icon(status.action_icon, class: status.action_class)
...@@ -43,10 +43,25 @@ ...@@ -43,10 +43,25 @@
%td.stage-cell %td.stage-cell
- pipeline.stages.each do |stage| - pipeline.stages.each do |stage|
- if stage.status - if stage.status
- tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - detailed_status = stage.detailed_status(current_user)
.stage-container - icon_status = "#{detailed_status.icon}_borderless"
= 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 - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
= ci_icon_for_status(stage.status)
.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 %td
- if pipeline.duration - if pipeline.duration
...@@ -66,7 +81,7 @@ ...@@ -66,7 +81,7 @@
.btn-group.inline .btn-group.inline
- if actions.any? - if actions.any?
.btn-group .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') = custom_icon('icon_play')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
...@@ -77,7 +92,7 @@ ...@@ -77,7 +92,7 @@
%span= build.name.humanize %span= build.name.humanize
- if artifacts.present? - if artifacts.present?
.btn-group .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("download")
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
......
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
.nothing-here-block No pipelines to show .nothing-here-block No pipelines to show
- else - else
.table-holder .table-holder
%table.table.ci-table %table.table.ci-table.js-pipeline-table
%tbody %thead
%th Status %th.pipeline-status Status
%th Pipeline %th.pipeline-info Pipeline
%th Commit %th.pipeline-commit Commit
%th Stages %th.pipeline-stages Stages
%th %th.pipeline-date
%th %th.pipeline-actions
= render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
%ul
- @stage.statuses.each do |status|
%li.dropdown-build
= render 'ci/status/graph_badge', subject: status
...@@ -42,14 +42,14 @@ ...@@ -42,14 +42,14 @@
.nothing-here-block No pipelines to show .nothing-here-block No pipelines to show
- else - else
.table-holder .table-holder
%table.table.ci-table %table.table.ci-table.js-pipeline-table
%thead %thead
%th Status %th.pipeline-status Status
%th Pipeline %th.pipeline-info Pipeline
%th Commit %th.pipeline-commit Commit
%th Stages %th.pipeline-stages Stages
%th %th.pipeline-date
%th.hidden-xs %th.pipeline-actions.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true = render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab' = paginate @pipelines, theme: 'gitlab'
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
---
title: Adds Direct link from pipeline list to builds
merge_request: 8097
author:
...@@ -139,6 +139,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -139,6 +139,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
member do member do
get :stage
post :cancel post :cancel
post :retry post :retry
get :builds get :builds
......
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
require 'spec_helper' require 'spec_helper'
describe "Pipelines", feature: true, js: true do describe 'Pipeline', :feature, :js do
include GitlabRoutingHelper include GitlabRoutingHelper
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
......
require 'spec_helper' require 'spec_helper'
describe "Pipelines" do describe 'Pipelines', :feature, :js do
include GitlabRoutingHelper include GitlabRoutingHelper
include WaitForAjax
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -69,16 +70,32 @@ describe "Pipelines" do ...@@ -69,16 +70,32 @@ describe "Pipelines" do
end end
context 'with manual actions' do 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 expect(page).to have_link('Manual build')
before { click_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
end end
...@@ -131,7 +148,10 @@ describe "Pipelines" do ...@@ -131,7 +148,10 @@ describe "Pipelines" do
before { visit namespace_project_pipelines_path(project.namespace, project) } before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') } 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 end
context 'with artifacts expired' do context 'with artifacts expired' do
...@@ -150,6 +170,42 @@ describe "Pipelines" do ...@@ -150,6 +170,42 @@ describe "Pipelines" do
it { expect(page).not_to have_selector('.build-artifacts') } it { expect(page).not_to have_selector('.build-artifacts') }
end end
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 end
describe 'POST /:project/pipelines' do describe 'POST /:project/pipelines' do
......
%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
/* 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');
});
});
});
})();
...@@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do ...@@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do
end end
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 describe 'state machine' do
let(:current) { Time.now.change(usec: 0) } let(:current) { Time.now.change(usec: 0) }
let(:build) { create_build('build1', 0) } let(:build) { create_build('build1', 0) }
......
...@@ -28,6 +28,19 @@ describe Ci::Stage, models: true do ...@@ -28,6 +28,19 @@ describe Ci::Stage, models: true do
end end
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 describe '#builds' do
let!(:stage_build) { create_job(:ci_build) } let!(:stage_build) { create_job(:ci_build) }
let!(:commit_status) { create_job(:commit_status) } let!(:commit_status) { create_job(:commit_status) }
......
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment