diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 11adf2568a1..889366d6ddf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -98,7 +98,7 @@ @extend .dropdown-toggle; padding-right: 20px; position: relative; - width: 160px; + width: 163px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9a4685504ff..267fcd77b38 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -524,3 +524,9 @@ $body-text-shadow: rgba(255,255,255,0.01); */ $ui-dev-kit-example-color: #bbb; $ui-dev-kit-example-border: #ddd; + +/* +Pipeline Graph +*/ +$stage-hover-bg: #eaf3fc; +$stage-hover-border: #d1e7fc; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d5f9a7088be..be22e7bdc79 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -288,15 +288,40 @@ } // Pipeline visualization +.pipeline-actions { + border-bottom: none; +} -.toggle-pipeline-btn { - background-color: $white-normal; +.tab-pane { + &.pipelines { + .ci-table { + min-width: 900px; + } - &.graph-collapsed { - background-color: $white-light; + .content-list.pipelines { + overflow: auto; + } + + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } + } + + &.builds { + .ci-table { + tr { + height: 71px; + } + } } } +// Pipeline graph .pipeline-graph { width: 100%; background-color: $gray-light; @@ -305,52 +330,120 @@ white-space: nowrap; transition: max-height 0.3s, padding 0.3s; - &.graph-collapsed { - max-height: 0; - padding: 0 16px; - } -} - -.pipeline-visualization { - position: relative; - ul { padding: 0; } -} -.stage-column { - display: inline-block; - vertical-align: top; - - &:not(:last-child) { - margin-right: 44px; + a { + text-decoration: none; + color: $gl-text-color-light; } - &.left-margin { - &:not(:first-child) { - margin-left: 44px; + svg { + vertical-align: middle; + margin-right: 3px; + } - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -48px; - border-top: 2px solid $border-color; - width: 48px; - height: 1px; + .stage-column { + display: inline-block; + vertical-align: top; + + &:not(:last-child) { + margin-right: 44px; + } + + &.left-margin { + &:not(:first-child) { + margin-left: 44px; + + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } } } } - } - &.no-margin { - margin: 0; - } + &.no-margin { + margin: 0; + } - li { - list-style: none; + li { + list-style: none; + } + + &:last-child { + .build { + // Remove right connecting horizontal line from first build in last stage + &:first-child { + &::after { + border: none; + } + } + // Remove right curved connectors from all builds in last stage + &:not(:first-child) { + &::after { + border: none; + } + } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } + } + } + + &:first-child { + .build { + // Remove left curved connectors from all builds in first stage + &:not(:first-child) { + &::before { + border: none; + } + } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -32px; + border-top: 2px solid $border-color; + } + + &::after { + left: -44px; + border-right: 2px solid $border-color; + border-radius: 0 20px; + } + + &::before { + right: -44px; + border-left: 2px solid $border-color; + border-radius: 20px 0 0; + } + } } .stage-name { @@ -364,166 +457,69 @@ .build { border: 1px solid $border-color; + border-radius: 30px; background-color: $white-light; position: relative; - padding: 7px 10px 8px; - border-radius: 30px; + padding: 8px 4px 9px 10px; width: 186px; margin-bottom: 10px; + white-space: normal; &:hover { - background-color: $gray-lighter; - } - - &.playable { - - svg { - height: 13px; - width: 20px; - position: relative; - top: 1px; - - path { - fill: $layout-link-gray; - } - } - } - - .build-content { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - width: 164px; - - .ci-status-icon { - svg { - height: 20px; - width: 20px; - } - } - - .tooltip { - white-space: nowrap; - - .tooltip-inner { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .ci-status-text { - width: 135px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - display: inline-block; - position: relative; - top: -1px; - } - - a { - color: $gl-text-color-light; - text-decoration: none; - } + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + a, + .dropdown-counter-badge, .dropdown-menu-toggle { - background-color: transparent; - border: none; - width: auto; - padding: 0; - color: $gl-text-color-light; - flex-grow: 1; - - .ci-status-text { - max-width: 112px; - width: auto; - } + color: $gl-text-color; } - .grouped-pipeline-dropdown { - padding: 0; - width: 186px; - left: auto; - right: -197px; - top: -9px; + .grouped-pipeline-dropdown a { + color: $gl-text-color-light; - ul { - max-height: 245px; - overflow: auto; - - li:first-child { - padding-top: 8px; - } - - li:last-child { - padding-bottom: 8px; - } - } - - a { + &:hover { color: $gl-text-color; - padding: 7px 8px 8px; - - &:hover { - background-color: $blue-light-transparent; - border-radius: 3px; - - .ci-status-text { - text-decoration: none; - } - } } - - svg { - width: 14px; - height: 14px; - } - - .ci-status-text { - width: 112px; - } - - .arrow { - &::before, - &::after { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 18px; - } - - &::before { - left: -5px; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $border-color; - } - - &::after { - left: -4px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - } - } - } - - .badge { - background-color: $gray-darker; - color: $gl-text-color-light; - font-weight: normal; - margin-left: $btn-xs-side-margin; } } - svg { - vertical-align: middle; - margin-right: 5px; + .ci-status-icon { + position: relative; + top: 1px; + } + + .ci-status-icon svg { + height: 20px; + width: 20px; + } + + .arrow { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 18px; + } + + &::before { + left: -5px; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: $border-color; + } + + &::after { + left: -4px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: $white-light; + } } // Connect first build in each stage with right horizontal line @@ -532,7 +528,7 @@ content: ''; position: absolute; top: 48%; - right: -48px; + right: -49px; border-top: 2px solid $border-color; width: 48px; height: 1px; @@ -580,109 +576,161 @@ } } - &:last-child { - .build { - // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: none; - } - } - // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: none; - } - } - // Remove opposite curve - .curve { - &::before { - display: none; - } + .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; + } + + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-light; + white-space: normal; + overflow: visible; + + &:focus { + outline: none; + } + + &:hover { + color: $gl-text-color; + + .dropdown-counter-badge { + color: $gl-text-color; } } } - &:first-child { - .build { - // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: none; + .dropdown-counter-badge { + float: right; + clear: right; + color: $border-color; + font-weight: 100; + font-size: 15px; + margin-right: 2px; + } + + .grouped-pipeline-dropdown { + padding: 0; + width: 191px; + left: auto; + right: -206px; + top: -11px; + box-shadow: 0 1px 5px $black-transparent; + + a { + display: inline-block; + + &:hover { + background-color: $stage-hover-bg; + } + } + + 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; + } + } + + .dropdown-build { + color: $gl-text-color-light; + + a.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; } } - // Remove opposite curve - .curve { - &::after { - display: none; + + &: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; + } } } + + .stage { + max-width: 100px; + width: 100px; + } + + .ci-status-icon svg { + height: 18px; + width: 18px; + } + + .ci-status-text { + max-width: 95px; + padding-bottom: 3px; + position: relative; + top: 3px; + } } } +} - // Curve first child connecting lines in opposite direction - .curve { - display: none; +// Action Icons +.ci-action-icon-container .ci-action-icon-wrapper { + float: right; + margin-top: -4px; - &::before, - &::after { - content: ''; - width: 21px; - height: 25px; - position: absolute; - top: -32px; - border-top: 2px solid $border-color; - } - - &::after { - left: -44px; - border-right: 2px solid $border-color; - border-radius: 0 20px; - } + 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 { - right: -44px; - border-left: 2px solid $border-color; - border-radius: 20px 0 0; - } - } -} - -.pipeline-actions { - border-bottom: none; -} - -.toggle-pipeline-btn { - - .fa { - color: $gl-gray-light; - } -} - -.tab-pane { - - &.pipelines { - - .ci-table { - min-width: 900px; + position: relative; + top: 3px; + left: 3px; } - .stage { - max-width: 100px; - width: 100px; - } - - .pipeline-actions { - min-width: initial; + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; } } - &.builds { - - .ci-table { - tr { - height: 71px; - } - } + .ci-play-icon { + padding: 5px 5px 5px 7px; } } diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml new file mode 100644 index 00000000000..c7d04ab61e9 --- /dev/null +++ b/app/views/ci/status/_graph_badge.html.haml @@ -0,0 +1,19 @@ +-# Renders the graph node with both the status icon, status name and action icon + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status}" + +- if status.has_details? + = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do + %span{ class: klass }= custom_icon(status.icon) + .ci-status-text= subject.name +- else + %span{ class: klass }= custom_icon(status.icon) + .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 + %i.ci-action-icon-wrapper + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml deleted file mode 100644 index ad1a7360a8b..00000000000 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- is_playable = subject.playable? && can?(current_user, :update_build, @project) -- if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do - = ci_icon_for_status('play') - .ci-status-text= subject.name -- elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - .ci-status-text= subject.name -- else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml deleted file mode 100644 index 1bba0443154..00000000000 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - - if subject.target_url - = link_to subject.target_url do - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name - - else - %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} - = ci_icon_for_status(subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index bf8c75b6e5c..b70b574e687 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -10,11 +10,10 @@ - status_groups.each do |group_name, grouped_statuses| - if grouped_statuses.one? - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } + %li.build .curve .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status + = render 'ci/status/graph_badge', subject: status - else %li.build .curve diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 2b26ad9d6fa..b03837d1211 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -4,10 +4,10 @@ = ci_icon_for_status(group_status) %span.ci-status-text = name - %span.badge= subject.size + %span.dropdown-counter-badge= subject.size .dropdown-menu.grouped-pipeline-dropdown .arrow %ul - subject.each do |status| - %li - = render "projects/#{status.to_partial_path}_pipeline", subject: status + %li.dropdown-build + = render 'ci/status/graph_badge', subject: status diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg old mode 100644 new mode 100755 index 41a210a8ed9..bd5d04e1cd7 --- a/app/views/shared/icons/_icon_status_canceled.svg +++ b/app/views/shared/icons/_icon_status_canceled.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg old mode 100644 new mode 100755 index 1f5c3b51b03..326ad04e017 --- a/app/views/shared/icons/_icon_status_created.svg +++ b/app/views/shared/icons/_icon_status_created.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg old mode 100644 new mode 100755 index af267b8938a..64da5aa31fc --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg old mode 100644 new mode 100755 index 516231d1b44..02d5da407e3 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg old mode 100644 new mode 100755 index d2618bce200..532f4fee33c --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg old mode 100644 new mode 100755 index 701f33bcbea..1998dfef9ea --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg old mode 100644 new mode 100755 index b7c21ba6971..eed5006bebe --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ - + diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg old mode 100644 new mode 100755 index 9191e0050a6..cb785635b7e --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1 +1 @@ - + diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml new file mode 100644 index 00000000000..7335e597292 --- /dev/null +++ b/changelogs/unreleased/22604-manual-actions.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "Manual actions on pipeline graph" +merge_request: 7931 +author: diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 23a504ff965..8f561c8f90b 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -107,7 +107,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do visit ci_status_path(pipeline) - click_on 'Cancel' + find('a.btn[title="Cancel"]').click expect(page).to have_content 'canceled' end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3350a3aeefc..0a77eaa123c 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -38,6 +38,91 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end + describe 'pipeline graph' do + context 'when pipeline has running builds' do + it 'shows a running icon and a cancel action for the running build' do + page.within('a[data-title="deploy - running"]') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content('deploy') + end + + page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + end + end + + it 'should be possible to cancel the running build' do + find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Cancel running') + end + end + + context 'when pipeline has successful builds' do + it 'shows the success icon and a retry action for the successfull build' do + page.within('a[data-title="build - passed"]') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + end + + page.within('a[data-title="build - passed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the success build' do + find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has failed builds' do + it 'shows the failed icon and a retry action for the failed build' do + page.within('a[data-title="test - failed"]') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + end + + page.within('a[data-title="test - failed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the failed build' do + find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has manual builds' do + it 'shows the skipped icon and a play action for the manual build' do + page.within('a[data-title="manual build - manual play action"]') do + expect(page).to have_selector('.ci-status-icon-skipped') + expect(page).to have_content('manual') + end + + page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-play') + end + end + + it 'should be possible to play the manual build' do + find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Play build') + end + end + + context 'when pipeline has external build' do + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end + end + end + context 'page tabs' do it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline')