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:
commit
54bacb1860
14 changed files with 191 additions and 94 deletions
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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'};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
17
app/views/projects/jobs/_empty_state.html.haml
Normal file
17
app/views/projects/jobs/_empty_state.html.haml
Normal 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
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue