Commit 54bacb18 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '22643-manual-job-page' into 'master'

Resolve "Improve non-triggered manual action job detail page"

Closes #22643 and #37843

See merge request gitlab-org/gitlab-ce!15991
parents a7a7f8b1 01d42763
...@@ -30,6 +30,9 @@ ...@@ -30,6 +30,9 @@
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length; return !this.isLoading && Object.keys(this.job).length;
}, },
jobStarted() {
return this.job.started;
},
}, },
methods: { methods: {
getActions() { getActions() {
...@@ -63,8 +66,9 @@ ...@@ -63,8 +66,9 @@
:time="job.created_at" :time="job.created_at"
:user="job.user" :user="job.user"
:actions="actions" :actions="actions"
:hasSidebarButton="true" :has-sidebar-button="true"
/> :should-render-triggered-label="jobStarted"
/>
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
size="2" size="2"
......
...@@ -45,6 +45,11 @@ export default { ...@@ -45,6 +45,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
}, },
directives: { directives: {
...@@ -82,7 +87,12 @@ export default { ...@@ -82,7 +87,12 @@ export default {
{{itemName}} #{{itemId}} {{itemName}} #{{itemId}}
</strong> </strong>
triggered <template v-if="shouldRenderTriggeredLabel">
triggered
</template>
<template v-else>
created
</template>
<timeago-tooltip :time="time" /> <timeago-tooltip :time="time" />
......
...@@ -20,10 +20,13 @@ ...@@ -20,10 +20,13 @@
width: 100%; width: 100%;
} }
&.svg-250 { $image-widths: 250 306 394;
img, @each $width in $image-widths {
svg { &.svg-#{$width} {
width: 250px; img,
svg {
width: #{$width + 'px'};
}
} }
} }
} }
......
...@@ -4,6 +4,8 @@ class JobEntity < Grape::Entity ...@@ -4,6 +4,8 @@ class JobEntity < Grape::Entity
expose :id expose :id
expose :name expose :name
expose :started?, as: :started
expose :build_path do |build| expose :build_path do |build|
build.target_url || path_to(:namespace_project_job, build) build.target_url || path_to(:namespace_project_job, build)
end end
......
- illustration = local_assigns.fetch(:illustration)
- illustration_size = local_assigns.fetch(:illustration_size)
- title = local_assigns.fetch(:title)
- content = local_assigns.fetch(:content)
- action = local_assigns.fetch(:action, nil)
.row.empty-state
.col-xs-12
.svg-content{ class: illustration_size }
= image_tag illustration
.col-xs-12
.text-content
%h4.text-center= title
%p= content
- if action
.text-center
= action
...@@ -54,41 +54,53 @@ ...@@ -54,41 +54,53 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.started?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
.build-trace-container.prepend-top-default .controllers.pull-right
.top-bar.js-top-bar - if @build.has_trace?
.js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< = link_to raw_project_job_path(@project, @build),
Showing last title: 'Show complete raw',
%span.js-truncated-info-size.truncated-info-size>< data: { placement: 'top', container: 'body' },
of log - class: 'js-raw-link-controller has-tooltip controllers-buttons' do
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw = icon('file-text-o')
.controllers.pull-right - if @build.erasable? && can?(current_user, :erase_build, @build)
- if @build.has_trace? = link_to erase_project_job_path(@project, @build),
= link_to raw_project_job_path(@project, @build), method: :post,
title: 'Show complete raw', data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
data: { placement: 'top', container: 'body' }, title: 'Erase job log',
class: 'js-raw-link-controller has-tooltip controllers-buttons' do class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('file-text-o') = icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
- if @build.erasable? && can?(current_user, :erase_build, @build) %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= link_to erase_project_job_path(@project, @build), = custom_icon('scroll_up')
method: :post, .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
title: 'Erase job log', = custom_icon('scroll_down')
class: 'has-tooltip js-erase-link controllers-buttons' do
= icon('trash')
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
- elsif @build.playable?
= render 'empty_state',
illustration: 'illustrations/manual_action.svg',
illustration_size: 'svg-394',
title: _('This job requires a manual action'),
content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.'),
action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), class: 'btn btn-primary', title: _('Trigger this manual action') )
- else
= render 'empty_state',
illustration: 'illustrations/job_not_triggered.svg',
illustration_size: 'svg-306',
title: _('This job has not been triggered yet'),
content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
= render "sidebar" = render "sidebar"
......
...@@ -11,7 +11,7 @@ module SharedBuilds ...@@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do step 'project has a recent build' do
@pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
@build = create(:ci_build, :coverage, pipeline: @pipeline) @build = create(:ci_build, :running, :coverage, pipeline: @pipeline)
end end
step 'recent build is successful' do step 'recent build is successful' do
......
...@@ -7,12 +7,10 @@ FactoryBot.define do ...@@ -7,12 +7,10 @@ FactoryBot.define do
stage_idx 0 stage_idx 0
ref 'master' ref 'master'
tag false tag false
status 'pending'
created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a' commands 'ls -a'
protected false protected false
created_at 'Di 29. Okt 09:50:00 CET 2013'
pending
options do options do
{ {
...@@ -29,23 +27,37 @@ FactoryBot.define do ...@@ -29,23 +27,37 @@ FactoryBot.define do
pipeline factory: :ci_pipeline pipeline factory: :ci_pipeline
trait :started do
started_at 'Di 29. Okt 09:51:28 CET 2013'
end
trait :finished do
started
finished_at 'Di 29. Okt 09:53:28 CET 2013'
end
trait :success do trait :success do
finished
status 'success' status 'success'
end end
trait :failed do trait :failed do
finished
status 'failed' status 'failed'
end end
trait :canceled do trait :canceled do
finished
status 'canceled' status 'canceled'
end end
trait :skipped do trait :skipped do
started
status 'skipped' status 'skipped'
end end
trait :running do trait :running do
started
status 'running' status 'running'
end end
...@@ -114,11 +126,6 @@ FactoryBot.define do ...@@ -114,11 +126,6 @@ FactoryBot.define do
build.project ||= build.pipeline.project build.project ||= build.pipeline.project
end end
factory :ci_not_started_build do
started_at nil
finished_at nil
end
trait :tag do trait :tag do
tag true tag true
end end
......
require 'spec_helper' require 'spec_helper'
describe 'User browses a job', :js do describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } let!(:build) { create(:ci_build, :running, :coverage, pipeline: pipeline) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -369,6 +369,34 @@ feature 'Jobs' do ...@@ -369,6 +369,34 @@ feature 'Jobs' do
end end
end end
end end
context 'Playable manual action' do
let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
before do
project.add_developer(user)
visit project_job_path(project, job)
end
it 'shows manual action empty state' do
expect(page).to have_content('This job requires a manual action')
expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.')
expect(page).to have_link('Trigger this manual action')
end
end
context 'Non triggered job' do
let(:job) { create(:ci_build, :created, pipeline: pipeline) }
before do
visit project_job_path(project, job)
end
it 'shows manual action empty state' do
expect(page).to have_content('This job has not been triggered yet')
expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered.')
end
end
end end
describe "POST /:project/jobs/:id/cancel", :js do describe "POST /:project/jobs/:id/cancel", :js do
......
import Vue from 'vue'; import Vue from 'vue';
import headerComponent from '~/jobs/components/header.vue'; import headerComponent from '~/jobs/components/header.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Job details header', () => { describe('Job details header', () => {
let HeaderComponent; let HeaderComponent;
...@@ -35,7 +36,7 @@ describe('Job details header', () => { ...@@ -35,7 +36,7 @@ describe('Job details header', () => {
isLoading: false, isLoading: false,
}; };
vm = new HeaderComponent({ propsData: props }).$mount(); vm = mountComponent(HeaderComponent, props);
}); });
afterEach(() => { afterEach(() => {
......
import Vue from 'vue'; import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue'; import headerCi from '~/vue_shared/components/header_ci_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Header CI Component', () => { describe('Header CI Component', () => {
let HeaderCi; let HeaderCi;
...@@ -8,7 +9,6 @@ describe('Header CI Component', () => { ...@@ -8,7 +9,6 @@ describe('Header CI Component', () => {
beforeEach(() => { beforeEach(() => {
HeaderCi = Vue.extend(headerCi); HeaderCi = Vue.extend(headerCi);
props = { props = {
status: { status: {
group: 'failed', group: 'failed',
...@@ -45,54 +45,65 @@ describe('Header CI Component', () => { ...@@ -45,54 +45,65 @@ describe('Header CI Component', () => {
], ],
hasSidebarButton: true, hasSidebarButton: true,
}; };
vm = new HeaderCi({
propsData: props,
}).$mount();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
it('should render status badge', () => { describe('render', () => {
expect(vm.$el.querySelector('.ci-failed')).toBeDefined(); beforeEach(() => {
expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined(); vm = mountComponent(HeaderCi, props);
expect( });
vm.$el.querySelector('.ci-failed').getAttribute('href'),
).toEqual(props.status.details_path);
});
it('should render item name and id', () => { it('should render status badge', () => {
expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123'); expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
}); expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
expect(
vm.$el.querySelector('.ci-failed').getAttribute('href'),
).toEqual(props.status.details_path);
});
it('should render timeago date', () => { it('should render item name and id', () => {
expect(vm.$el.querySelector('time')).toBeDefined(); expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
}); });
it('should render user icon and name', () => { it('should render timeago date', () => {
expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); expect(vm.$el.querySelector('time')).toBeDefined();
}); });
it('should render provided actions', () => { it('should render user icon and name', () => {
expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label); });
expect(vm.$el.querySelector('.link').tagName).toEqual('A');
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); it('should render provided actions', () => {
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
}); expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
expect(vm.$el.querySelector('.link').tagName).toEqual('A');
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
});
it('should show loading icon', (done) => { it('should show loading icon', (done) => {
vm.actions[0].isLoading = true; vm.actions[0].isLoading = true;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
done(); done();
});
});
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
}); });
}); });
it('should render sidebar toggle button', () => { describe('shouldRenderTriggeredLabel', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
expect(vm.$el.textContent).toContain('created');
expect(vm.$el.textContent).not.toContain('triggered');
});
}); });
}); });
...@@ -122,17 +122,18 @@ describe 'cycle analytics events' do ...@@ -122,17 +122,18 @@ describe 'cycle analytics events' do
let(:stage) { :test } let(:stage) { :test }
let(:merge_request) { MergeRequest.first } let(:merge_request) { MergeRequest.first }
let!(:pipeline) do let!(:pipeline) do
create(:ci_pipeline, create(:ci_pipeline,
ref: merge_request.source_branch, ref: merge_request.source_branch,
sha: merge_request.diff_head_sha, sha: merge_request.diff_head_sha,
project: context.project, project: project,
head_pipeline_of: merge_request) head_pipeline_of: merge_request)
end end
before do before do
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, :success, pipeline: pipeline, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run! pipeline.run!
pipeline.succeed! pipeline.succeed!
...@@ -219,17 +220,18 @@ describe 'cycle analytics events' do ...@@ -219,17 +220,18 @@ describe 'cycle analytics events' do
describe '#staging_events' do describe '#staging_events' do
let(:stage) { :staging } let(:stage) { :staging }
let(:merge_request) { MergeRequest.first } let(:merge_request) { MergeRequest.first }
let!(:pipeline) do let!(:pipeline) do
create(:ci_pipeline, create(:ci_pipeline,
ref: merge_request.source_branch, ref: merge_request.source_branch,
sha: merge_request.diff_head_sha, sha: merge_request.diff_head_sha,
project: context.project, project: project,
head_pipeline_of: merge_request) head_pipeline_of: merge_request)
end end
before do before do
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, :success, pipeline: pipeline, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run! pipeline.run!
pipeline.succeed! pipeline.succeed!
......
...@@ -11,7 +11,7 @@ describe API::Jobs do ...@@ -11,7 +11,7 @@ describe API::Jobs do
ref: project.default_branch) ref: project.default_branch)
end end
let!(:job) { create(:ci_build, pipeline: pipeline) } let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:api_user) { user } let(:api_user) { user }
...@@ -443,7 +443,7 @@ describe API::Jobs do ...@@ -443,7 +443,7 @@ describe API::Jobs do
context 'user with :update_build persmission' do context 'user with :update_build persmission' do
it 'cancels running or pending job' do it 'cancels running or pending job' do
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(project.builds.first.status).to eq('canceled') expect(project.builds.first.status).to eq('success')
end end
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