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
This commit is contained in:
Kamil Trzciński 2018-01-06 14:40:45 +00:00
commit 54bacb1860
14 changed files with 191 additions and 94 deletions

View file

@ -30,6 +30,9 @@
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
jobStarted() {
return this.job.started;
},
},
methods: {
getActions() {
@ -63,8 +66,9 @@
:time="job.created_at"
:user="job.user"
:actions="actions"
:hasSidebarButton="true"
/>
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
/>
<loading-icon
v-if="isLoading"
size="2"

View file

@ -45,6 +45,11 @@ export default {
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
},
directives: {
@ -82,7 +87,12 @@ export default {
{{itemName}} #{{itemId}}
</strong>
triggered
<template v-if="shouldRenderTriggeredLabel">
triggered
</template>
<template v-else>
created
</template>
<timeago-tooltip :time="time" />

View file

@ -20,10 +20,13 @@
width: 100%;
}
&.svg-250 {
img,
svg {
width: 250px;
$image-widths: 250 306 394;
@each $width in $image-widths {
&.svg-#{$width} {
img,
svg {
width: #{$width + 'px'};
}
}
}
}

View file

@ -4,6 +4,8 @@ class JobEntity < Grape::Entity
expose :id
expose :name
expose :started?, as: :started
expose :build_path do |build|
build.target_url || path_to(:namespace_project_job, build)
end

View file

@ -0,0 +1,17 @@
- 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

View file

@ -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)}
- else
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
.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
.controllers.pull-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
.controllers.pull-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log',
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
- if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase job log',
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
- 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"

View file

@ -11,7 +11,7 @@ module SharedBuilds
step 'project has a recent build' do
@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
step 'recent build is successful' do

View file

@ -7,12 +7,10 @@ FactoryBot.define do
stage_idx 0
ref 'master'
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'
protected false
created_at 'Di 29. Okt 09:50:00 CET 2013'
pending
options do
{
@ -29,23 +27,37 @@ FactoryBot.define do
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
finished
status 'success'
end
trait :failed do
finished
status 'failed'
end
trait :canceled do
finished
status 'canceled'
end
trait :skipped do
started
status 'skipped'
end
trait :running do
started
status 'running'
end
@ -114,11 +126,6 @@ FactoryBot.define do
build.project ||= build.pipeline.project
end
factory :ci_not_started_build do
started_at nil
finished_at nil
end
trait :tag do
tag true
end

View file

@ -1,7 +1,7 @@
require 'spec_helper'
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(:project) { create(:project, :repository, namespace: user.namespace) }
let(:user) { create(:user) }

View file

@ -369,6 +369,34 @@ feature 'Jobs' do
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
describe "POST /:project/jobs/:id/cancel", :js do

View file

@ -1,5 +1,6 @@
import Vue from 'vue';
import headerComponent from '~/jobs/components/header.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Job details header', () => {
let HeaderComponent;
@ -35,7 +36,7 @@ describe('Job details header', () => {
isLoading: false,
};
vm = new HeaderComponent({ propsData: props }).$mount();
vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {

View file

@ -1,5 +1,6 @@
import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Header CI Component', () => {
let HeaderCi;
@ -8,7 +9,6 @@ describe('Header CI Component', () => {
beforeEach(() => {
HeaderCi = Vue.extend(headerCi);
props = {
status: {
group: 'failed',
@ -45,54 +45,65 @@ describe('Header CI Component', () => {
],
hasSidebarButton: true,
};
vm = new HeaderCi({
propsData: props,
}).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render status badge', () => {
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);
});
describe('render', () => {
beforeEach(() => {
vm = mountComponent(HeaderCi, props);
});
it('should render item name and id', () => {
expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
});
it('should render status badge', () => {
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', () => {
expect(vm.$el.querySelector('time')).toBeDefined();
});
it('should render item name and id', () => {
expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
});
it('should render user icon and name', () => {
expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
});
it('should render timeago date', () => {
expect(vm.$el.querySelector('time')).toBeDefined();
});
it('should render provided actions', () => {
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 render user icon and name', () => {
expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
});
it('should show loading icon', (done) => {
vm.actions[0].isLoading = true;
it('should render provided actions', () => {
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);
});
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
done();
it('should show loading icon', (done) => {
vm.actions[0].isLoading = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
done();
});
});
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
});
});
it('should render sidebar toggle button', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined();
describe('shouldRenderTriggeredLabel', () => {
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');
});
});
});

View file

@ -122,17 +122,18 @@ describe 'cycle analytics events' do
let(:stage) { :test }
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
project: context.project,
project: project,
head_pipeline_of: merge_request)
end
before do
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, :success, pipeline: pipeline, author: user)
create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run!
pipeline.succeed!
@ -219,17 +220,18 @@ describe 'cycle analytics events' do
describe '#staging_events' do
let(:stage) { :staging }
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
project: context.project,
project: project,
head_pipeline_of: merge_request)
end
before do
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, :success, pipeline: pipeline, author: user)
create(:ci_build, :success, pipeline: pipeline, author: user)
pipeline.run!
pipeline.succeed!

View file

@ -11,7 +11,7 @@ describe API::Jobs do
ref: project.default_branch)
end
let!(:job) { create(:ci_build, pipeline: pipeline) }
let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
let(:user) { create(:user) }
let(:api_user) { user }
@ -443,7 +443,7 @@ describe API::Jobs do
context 'user with :update_build persmission' do
it 'cancels running or pending job' do
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