Commit 59fa031e authored by Jacob Schatz's avatar Jacob Schatz

Merge branch '18141-pipeline-graph' into 'master'

Add pipeline graph

## What does this MR do?
Adds pipeline visualization

## What are the relevant issue numbers?
Closes #18141    
Part of #19982 

## Screenshots (if relevant)
![Screen_Shot_2016-08-16_at_7.59.52_PM](/uploads/c9dd695d2ddbd2a85e98a5b4e500d52c/Screen_Shot_2016-08-16_at_7.59.52_PM.png)
![Screen_Shot_2016-08-16_at_7.55.49_PM](/uploads/5ab548cc5fc8a42371d3b54108798c02/Screen_Shot_2016-08-16_at_7.55.49_PM.png)

See merge request !5742
parents 30091edf 74f80465
...@@ -119,6 +119,7 @@ v 8.11.0 (unreleased) ...@@ -119,6 +119,7 @@ v 8.11.0 (unreleased)
- Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
- Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko) - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
- Adds support for pending invitation project members importing projects - Adds support for pending invitation project members importing projects
- Add pipeline visualization/graph on pipeline page
- Update devise initializer to turn on changed password notification emails. !5648 (tombell) - Update devise initializer to turn on changed password notification emails. !5648 (tombell)
- Avoid to show the original password field when password is automatically set. !5712 (duduribeiro) - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
- Fix importing GitLab projects with an invalid MR source project - Fix importing GitLab projects with an invalid MR source project
......
(function() {
function toggleGraph() {
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
}
$(document).on('click', '.toggle-pipeline-btn', toggleGraph);
})();
...@@ -230,6 +230,187 @@ ...@@ -230,6 +230,187 @@
} }
} }
// Pipeline visualization
.toggle-pipeline-btn {
background-color: $gray-dark;
.caret {
border-top: none;
border-bottom: 4px solid;
}
&.graph-collapsed {
background-color: $white-light;
.caret {
border-bottom: none;
border-top: 4px solid;
}
}
}
.pipeline-graph {
width: 100%;
overflow: auto;
white-space: nowrap;
max-height: 500px;
transition: max-height 0.3s, padding 0.3s;
&.graph-collapsed {
max-height: 0;
padding: 0 16px;
}
}
.pipeline-visualization {
position: relative;
min-width: 1220px;
ul {
padding: 0;
}
}
.stage-column {
display: inline-block;
vertical-align: top;
margin-right: 50px;
li {
list-style: none;
}
.stage-name {
margin-bottom: 15px;
font-weight: bold;
width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.build {
border: 1px solid $border-color;
position: relative;
padding: 6px 10px;
border-radius: 30px;
width: 150px;
margin-bottom: 10px;
&.playable {
background-color: $gray-light;
}
.build-content {
width: 130px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: $layout-link-gray;
}
}
svg {
position: relative;
top: 2px;
margin-right: 5px;
}
.fa {
font-size: 13px;
}
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
content: '';
position: absolute;
top: 50%;
right: -54px;
border-top: 2px solid $border-color;
width: 54px;
height: 1px;
}
}
// Connect each build (except for first) with curved lines
&:not(:first-child) {
&::after, &::before {
content: '';
top: -47px;
position: absolute;
border-bottom: 2px solid $border-color;
width: 20px;
height: 65px;
}
// Right connecting curves
&::after {
right: -20px;
border-right: 2px solid $border-color;
border-radius: 0 0 50px;
}
// Left connecting curves
&::before {
left: -20px;
border-left: 2px solid $border-color;
border-radius: 0 0 0 50px;
}
}
// Connect second build to first build with smaller curved line
&:nth-child(2) {
&::after, &::before {
height: 45px;
top: -26px;
}
}
}
&:last-child {
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after, &::before {
border: none;
}
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
border: none;
}
}
}
}
&:first-child {
.build {
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
border: none;
}
}
}
}
}
.pipeline-actions {
border-bottom: none;
}
.toggle-pipeline-btn {
.fa {
color: $dropdown-header-color;
}
}
.pipelines.tab-pane { .pipelines.tab-pane {
.content-list.pipelines { .content-list.pipelines {
......
...@@ -38,6 +38,10 @@ module CiStatusHelper ...@@ -38,6 +38,10 @@ module CiStatusHelper
'icon_status_pending' 'icon_status_pending'
when 'running' when 'running'
'icon_status_running' 'icon_status_running'
when 'play'
return icon('play fw')
when 'created'
'icon_status_pending'
else else
'icon_status_cancel' 'icon_status_cancel'
end end
...@@ -48,13 +52,13 @@ module CiStatusHelper ...@@ -48,13 +52,13 @@ module CiStatusHelper
def render_commit_status(commit, tooltip_placement: 'auto left') def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit) path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement) render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left') def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
project = pipeline.project project = pipeline.project
path = namespace_project_pipeline_path(project.namespace, project, pipeline) path = namespace_project_pipeline_path(project.namespace, project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement) render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
end end
def no_runners_for_project?(project) def no_runners_for_project?(project)
...@@ -62,13 +66,17 @@ module CiStatusHelper ...@@ -62,13 +66,17 @@ module CiStatusHelper
Ci::Runner.shared.blank? Ci::Runner.shared.blank?
end end
private def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement }
def render_status_with_link(type, status, path, tooltip_placement, cssclass: '') if path
link_to ci_icon_for_status(status), link_to ci_icon_for_status(status), path,
path, class: klass, title: title, data: data
class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}", else
title: "#{type.titleize}: #{ci_label_for_status(status)}", content_tag :span, ci_icon_for_status(status),
data: { toggle: 'tooltip', placement: tooltip_placement } class: klass, title: title, data: data
end
end end
end end
...@@ -97,7 +97,7 @@ module Ci ...@@ -97,7 +97,7 @@ module Ci
end end
def playable? def playable?
project.builds_enabled? && commands.present? && manual? project.builds_enabled? && commands.present? && manual? && skipped?
end end
def play(current_user = nil) def play(current_user = nil)
......
...@@ -78,6 +78,10 @@ module Ci ...@@ -78,6 +78,10 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages CommitStatus.where(pipeline: pluck(:id)).stages
end end
def stages_with_latest_statuses
statuses.latest.order(:stage_idx).group_by(&:stage)
end
def project_id def project_id
project.id project.id
end end
......
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
%li.build{class: ("playable" if is_playable)}
.build-content
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
= render_status_with_link('build', 'play')
= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
= render_status_with_link('build', subject.status)
= subject.name
- else
= render_status_with_link('build', subject.status)
= ci_icon_for_status(subject.status)
.row-content-block.build-content.middle-block .row-content-block.build-content.middle-block.pipeline-actions
.pull-right .pull-right
.btn.btn-grouped.btn-white.toggle-pipeline-btn
%span.toggle-btn-text Hide
%span pipeline graph
%span.caret
- if can?(current_user, :update_pipeline, pipeline.project) - if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?) - if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
...@@ -23,6 +27,22 @@ ...@@ -23,6 +27,22 @@
in in
= time_interval_in_words pipeline.duration = time_interval_in_words pipeline.duration
.row-content-block.build-content.middle-block.pipeline-graph
.pipeline-visualization
%ul.stage-column-list
- stages = pipeline.stages_with_latest_statuses
- stages.each do |stage, statuses|
%li.stage-column
.stage-name
%a{name: stage}
- if stage
= stage.titleize
.builds-container
%ul
- statuses.each do |status|
= render "projects/#{status.to_partial_path}_pipeline", subject: status
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger .bs-callout.bs-callout-danger
%h4 Found errors in your .gitlab-ci.yml: %h4 Found errors in your .gitlab-ci.yml:
......
%li.build
.build-content
- if subject.target_url
- link_to subject.target_url do
= render_status_with_link('commit status', subject.status)
= subject.name
- else
= render_status_with_link('commit status', subject.status)
= subject.name
class Gitlab::Seeder::Builds class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy] STAGES = %w[build test deploy notify]
BUILDS = [ BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success }, { name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success }, { name: 'build:osx', stage: 'build', status: :success },
{ name: 'slack post build', stage: 'notify_build', status: :success },
{ name: 'rspec:linux', stage: 'test', status: :success }, { name: 'rspec:linux', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:windows', stage: 'test', status: :success },
...@@ -12,9 +11,9 @@ class Gitlab::Seeder::Builds ...@@ -12,9 +11,9 @@ class Gitlab::Seeder::Builds
{ name: 'spinach:osx', stage: 'test', status: :canceled }, { name: 'spinach:osx', stage: 'test', status: :canceled },
{ name: 'cucumber:linux', stage: 'test', status: :running }, { name: 'cucumber:linux', stage: 'test', status: :running },
{ name: 'cucumber:osx', stage: 'test', status: :failed }, { name: 'cucumber:osx', stage: 'test', status: :failed },
{ name: 'slack post test', stage: 'notify_test', status: :success },
{ name: 'staging', stage: 'deploy', environment: 'staging', status: :success }, { name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
] ]
def initialize(project) def initialize(project)
...@@ -25,7 +24,7 @@ class Gitlab::Seeder::Builds ...@@ -25,7 +24,7 @@ class Gitlab::Seeder::Builds
pipelines.each do |pipeline| pipelines.each do |pipeline|
begin begin
BUILDS.each { |opts| build_create!(pipeline, opts) } BUILDS.each { |opts| build_create!(pipeline, opts) }
commit_status_create!(pipeline, name: 'jenkins', status: :success) commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
print '.' print '.'
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
print 'F' print 'F'
......
...@@ -193,7 +193,11 @@ describe "Pipelines" do ...@@ -193,7 +193,11 @@ describe "Pipelines" do
end end
context 'playing manual build' do context 'playing manual build' do
before { click_link('Play') } before do
within '.pipeline-holder' do
click_link('Play')
end
end
it { expect(@manual.reload).to be_pending } it { expect(@manual.reload).to be_pending }
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