Commit cf26610f authored by Phil Hughes's avatar Phil Hughes

Merge branch '50904-stages-sidebar' into 'master'

Moves stages dropdown into the new vue app

See merge request gitlab-org/gitlab-ce!21971
parents c4d9f402 f72a1bf0
...@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours { ...@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document); this.$document = $(document);
this.$window = $(window); this.$window = $(window);
this.logBytes = 0; this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
...@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours { ...@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.initSidebar(); this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize(); this.sidebarOnResize();
this.$document this.$document
.off('click', '.js-sidebar-build-toggle') .off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window this.$window
...@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours { ...@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
} }
// eslint-disable-next-line class-methods-use-this
populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
}
// eslint-disable-next-line class-methods-use-this
updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
}
updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
}
} }
<script> <script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -16,26 +17,39 @@ ...@@ -16,26 +17,39 @@
type: Array, type: Array,
required: true, required: true,
}, },
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="builds-container"> <div class="js-jobs-container builds-container">
<div <div
v-for="job in jobs"
:key="job.id"
class="build-job" class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
> >
<a <a
v-for="job in jobs"
:key="job.id"
v-tooltip v-tooltip
:href="job.path" :href="job.status.details_path"
:title="job.tooltip" :title="tooltipText(job)"
:class="{ active: job.active, retried: job.retried }" data-container="body"
> >
<icon <icon
v-if="job.active" v-if="isJobActive(job.id)"
name="arrow-right" name="arrow-right"
class="js-arrow-right" class="js-arrow-right icon-arrow-right"
/> />
<ci-icon :status="job.status" /> <ci-icon :status="job.status" />
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -7,26 +8,22 @@ ...@@ -7,26 +8,22 @@
import ArtifactsBlock from './artifacts_block.vue'; import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue'; import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue'; import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
export default { export default {
name: 'SidebarDetailsBlock', name: 'JobSidebar',
components: { components: {
ArtifactsBlock, ArtifactsBlock,
CommitBlock, CommitBlock,
DetailRow, DetailRow,
Icon, Icon,
TriggerBlock, TriggerBlock,
StagesDropdown,
JobsContainer,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: { runnerHelpUrl: {
type: String, type: String,
required: false, required: false,
...@@ -39,9 +36,7 @@ ...@@ -39,9 +36,7 @@
}, },
}, },
computed: { computed: {
shouldRenderContent() { ...mapState(['job', 'isLoading', 'stages', 'jobs']),
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() { coverage() {
return `${this.job.coverage}%`; return `${this.job.coverage}%`;
}, },
...@@ -97,20 +92,31 @@ ...@@ -97,20 +92,31 @@
}, },
hasStages() { hasStages() {
return ( return (
this.job && (this.job &&
this.job.pipeline && this.job.pipeline &&
this.job.pipeline.stages && this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0 this.job.pipeline.stages.length > 0) ||
) || false; false
);
}, },
commit() { commit() {
return this.job.pipeline.commit || {}; return this.job.pipeline.commit || {};
}, },
}, },
methods: {
...mapActions(['fetchJobsForStage']),
},
}; };
</script> </script>
<template> <template>
<div> <aside
class="right-sidebar right-sidebar-expanded build-sidebar"
data-offset-top="101"
data-spy="affix"
>
<div class="sidebar-container">
<div class="blocks-container">
<template v-if="!isLoading">
<div class="block"> <div class="block">
<strong class="inline prepend-top-8"> <strong class="inline prepend-top-8">
{{ job.name }} {{ job.name }}
...@@ -137,7 +143,8 @@ ...@@ -137,7 +143,8 @@
<button <button
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
type="button" type="button"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
> >
<i <i
aria-hidden="true" aria-hidden="true"
...@@ -146,7 +153,6 @@ ...@@ -146,7 +153,6 @@
></i> ></i>
</button> </button>
</div> </div>
<template v-if="shouldRenderContent">
<div <div
v-if="job.retry_path || job.new_issue_path" v-if="job.retry_path || job.new_issue_path"
class="block retry-link" class="block retry-link"
...@@ -168,7 +174,7 @@ ...@@ -168,7 +174,7 @@
{{ __('Retry') }} {{ __('Retry') }}
</a> </a>
</div> </div>
<div :class="{block : renderBlock }"> <div :class="{ block : renderBlock }">
<p <p
v-if="job.merge_request" v-if="job.merge_request"
class="build-detail-row js-job-mr" class="build-detail-row js-job-mr"
...@@ -266,11 +272,26 @@ ...@@ -266,11 +272,26 @@
:commit="commit" :commit="commit"
:merge-request="job.merge_request" :merge-request="job.merge_request"
/> />
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template> </template>
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-else
:size="2" :size="2"
class="prepend-top-10" class="prepend-top-10"
/> />
</div> </div>
<jobs-container
v-if="!isLoading && jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
</div>
</aside>
</template> </template>
<script> <script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import { sprintf, __ } from '~/locale';
export default { export default {
components: { components: {
...@@ -10,30 +10,14 @@ ...@@ -10,30 +10,14 @@
Icon, Icon,
}, },
props: { props: {
pipelineId: { pipeline: {
type: Number, type: Object,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
required: true, required: true,
}, },
stages: { stages: {
type: Array, type: Array,
required: true, required: true,
}, },
pipelineStatus: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -41,51 +25,68 @@ ...@@ -41,51 +25,68 @@
}; };
}, },
computed: { computed: {
pipelineLink() { hasRef() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { return !_.isEmpty(this.pipeline.ref);
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, },
pipelineId: this.pipelineId, },
pipelineLinkEnd: '</a>', watch: {
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, // When the component is initially mounted it may start with an empty stages array.
pipelineRef: this.pipelineRef, // Once the prop is updated, we set the first stage as the selected one
pipelineLinkRefEnd: '</a>', stages(newVal) {
}, false); if (newVal.length) {
this.selectedStage = newVal[0].name;
}
}, },
}, },
methods: { methods: {
onStageClick(stage) { onStageClick(stage) {
// todo: consider moving into store
this.selectedStage = stage.name;
// update dropdown with jobs
// jobs container is a new component.
this.$emit('requestSidebarStageDropdown', stage); this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="block-last"> <div class="block-last dropdown">
<ci-icon :status="pipelineStatus" /> <ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
<p v-html="pipelineLink"></p> {{ __('Pipeline') }}
<a
:href="pipeline.path"
class="js-pipeline-path link-commit"
>
#{{ pipeline.id }}
</a>
<template v-if="hasRef">
{{ __('from') }}
<a
:href="pipeline.ref.path"
class="link-commit ref-name"
>
{{ pipeline.ref.name }}
</a>
</template>
<div class="dropdown">
<button <button
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
> >
{{ selectedStage }} {{ selectedStage }}
<icon name="chevron-down" /> <i class="fa fa-chevron-down" ></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li <li
v-for="(stage, index) in stages" v-for="stage in stages"
:key="index" :key="stage.name"
> >
<button <button
type="button" type="button"
class="stage-item" class="js-stage-item stage-item"
@click="onStageClick(stage)" @click="onStageClick(stage)"
> >
{{ stage.name }} {{ stage.name }}
...@@ -93,5 +94,4 @@ ...@@ -93,5 +94,4 @@
</li> </li>
</ul> </ul>
</div> </div>
</div>
</template> </template>
import { mapState } from 'vuex'; import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue'; import Vue from 'vue';
import Job from '../job'; import Job from '../job';
import JobHeader from './components/header.vue'; import JobHeader from './components/header.vue';
import DetailsBlock from './components/sidebar_details_block.vue'; import Sidebar from './components/sidebar.vue';
import createStore from './store'; import createStore from './store';
export default () => { export default () => {
...@@ -13,6 +14,7 @@ export default () => { ...@@ -13,6 +14,7 @@ export default () => {
const store = createStore(); const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint); store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob'); store.dispatch('fetchJob');
// Header // Header
...@@ -43,17 +45,25 @@ export default () => { ...@@ -43,17 +45,25 @@ export default () => {
new Vue({ new Vue({
el: detailsBlockElement, el: detailsBlockElement,
components: { components: {
DetailsBlock, Sidebar,
}, },
store,
computed: { computed: {
...mapState(['job', 'isLoading']), ...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
}, },
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) { render(createElement) {
return createElement('details-block', { return createElement('sidebar', {
props: { props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl, runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath, terminalPath: detailsBlockDataset.terminalPath,
}, },
......
...@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => { ...@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => {
}); });
}; };
export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data); export const receiveJobSuccess = ({ commit }, data) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
};
export const receiveJobError = ({ commit }) => { export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR); commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.')); flash(__('An error occurred while fetching the job.'));
...@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => { ...@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages'); dispatch('requestStages');
axios axios
.get(state.stagesEndpoint) .get(state.job.pipeline.path)
.then(({ data }) => dispatch('receiveStagesSuccess', data)) .then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError')); .catch(() => dispatch('receiveStagesError'));
}; };
export const receiveStagesSuccess = ({ commit }, data) => export const receiveStagesSuccess = ({ commit }, data) =>
...@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => { ...@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown * Jobs list on sidebar - depend on stages dropdown
*/ */
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
// On stage click, set selected stage + fetch job // On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => { export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('setSelectedStage', stage);
dispatch('requestJobsForStage'); dispatch('requestJobsForStage');
axios axios
.get(state.stageJobsEndpoint) .get(stage.dropdown_path, {
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data)) params: {
retried: 1,
},
})
.then(({ data }) => {
const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
})
.catch(() => dispatch('receiveJobsForStageError')); .catch(() => dispatch('receiveJobsForStageError'));
}; };
export const receiveJobsForStageSuccess = ({ commit }, data) => export const receiveJobsForStageSuccess = ({ commit }, data) =>
......
...@@ -328,23 +328,6 @@ ...@@ -328,23 +328,6 @@
} }
} }
.build-dropdown {
margin: $gl-padding 0;
padding: 0;
.dropdown-menu-toggle {
margin-top: #{$gl-padding / 2};
}
svg {
position: relative;
top: 3px;
margin-right: 3px;
width: 14px;
height: 14px;
}
}
.builds-container { .builds-container {
background-color: $white-light; background-color: $white-light;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
...@@ -381,15 +364,11 @@ ...@@ -381,15 +364,11 @@
position: absolute; position: absolute;
left: 15px; left: 15px;
top: 20px; top: 20px;
display: none; display: block;
} }
&.active { &.active {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
} }
&.retried { &.retried {
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
- if @build.pipeline.stages_count > 1
.block-last.dropdown.build-dropdown
%div
%span{ class: "ci-status-icon-#{@build.pipeline.status}" }
= ci_icon_for_status(@build.pipeline.status)
Pipeline
= link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
from
= link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.legacy_stages.each do |stage|
%li
%a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = sanitize(build.tooltip_message.dup)
= link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
- if build.name
= build.name
- else
= build.id
- if build.retried?
= sprite_icon('retry', size:16, css_class: 'icon-retry')
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
- else - else
= render "empty_states" = render "empty_states"
= render "sidebar", builds: @builds #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options } .js-build-options{ data: javascript_build_options }
......
...@@ -4310,9 +4310,6 @@ msgstr "" ...@@ -4310,9 +4310,6 @@ msgstr ""
msgid "Pipeline" msgid "Pipeline"
msgstr "" msgstr ""
msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
msgstr ""
msgid "Pipeline Health" msgid "Pipeline Health"
msgstr "" msgstr ""
...@@ -7039,6 +7036,9 @@ msgstr "" ...@@ -7039,6 +7036,9 @@ msgstr ""
msgid "for this project" msgid "for this project"
msgstr "" msgstr ""
msgid "from"
msgstr ""
msgid "here" msgid "here"
msgstr "" msgstr ""
......
...@@ -60,7 +60,7 @@ describe 'Environment' do ...@@ -60,7 +60,7 @@ describe 'Environment' do
context 'with manual action' do context 'with manual action' do
let(:action) do let(:action) do
create(:ci_build, :manual, pipeline: pipeline, create(:ci_build, :manual, pipeline: pipeline,
name: 'deploy to production') name: 'deploy to production', environment: environment.name)
end end
context 'when user has ability to trigger deployment' do context 'when user has ability to trigger deployment' do
...@@ -73,12 +73,16 @@ describe 'Environment' do ...@@ -73,12 +73,16 @@ describe 'Environment' do
expect(page).to have_link(action.name.humanize) expect(page).to have_link(action.name.humanize)
end end
it 'does allow to play manual action' do it 'does allow to play manual action', :js do
expect(action).to be_manual expect(action).to be_manual
find('button.dropdown').click
expect { click_link(action.name.humanize) } expect { click_link(action.name.humanize) }
.not_to change { Ci::Pipeline.count } .not_to change { Ci::Pipeline.count }
wait_for_all_requests
expect(page).to have_content(action.name) expect(page).to have_content(action.name)
expect(action.reload).to be_pending expect(action.reload).to be_pending
end end
...@@ -165,10 +169,10 @@ describe 'Environment' do ...@@ -165,10 +169,10 @@ describe 'Environment' do
name: action.ref, project: project) name: action.ref, project: project)
end end
it 'allows to stop environment' do it 'allows to stop environment', :js do
click_button('Stop') click_button('Stop')
click_button('Stop environment') # confirm modal click_button('Stop environment') # confirm modal
wait_for_all_requests
expect(page).to have_content('close_app') expect(page).to have_content('close_app')
end end
end end
......
...@@ -38,9 +38,10 @@ describe 'User browses a job', :js do ...@@ -38,9 +38,10 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) } let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason' do it 'displays the failure reason' do
wait_for_all_requests
within('.builds-container') do within('.builds-container') do
build_link = first('.build-job > a') build_link = first('.build-job > a')
expect(build_link['data-title']).to eq('test - failed - (unknown failure)') expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)')
end end
end end
end end
...@@ -49,9 +50,10 @@ describe 'User browses a job', :js do ...@@ -49,9 +50,10 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) } let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
it 'displays the failure reason and retried label' do it 'displays the failure reason and retried label' do
wait_for_all_requests
within('.builds-container') do within('.builds-container') do
build_link = first('.build-job > a') build_link = first('.build-job > a')
expect(build_link['data-title']).to eq('test - failed - (unknown failure) (retried)') expect(build_link['data-original-title']).to eq('test - failed - (unknown failure) (retried)')
end end
end end
end end
......
...@@ -134,23 +134,25 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -134,23 +134,25 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).to have_content pipeline.commit.title expect(page).to have_content pipeline.commit.title
end end
it 'shows active job' do it 'shows active job', :js do
visit project_job_path(project, job) visit project_job_path(project, job)
wait_for_requests
expect(page).to have_selector('.build-job.active') expect(page).to have_selector('.build-job.active')
end end
end end
context 'sidebar' do context 'sidebar', :js do
let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline, name: '<img src=x onerror=alert(document.domain)>') } let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline, name: '<img src=x onerror=alert(document.domain)>') }
before do before do
visit project_job_path(project, job) visit project_job_path(project, job)
wait_for_requests
end end
it 'renders escaped tooltip name' do it 'renders escaped tooltip name' do
page.within('aside.right-sidebar') do page.within('aside.right-sidebar') do
expect(find('.active.build-job a')['data-title']).to eq('<img src="x"> - passed') expect(find('.active.build-job a')['data-original-title']).to eq('&lt;img src=x onerror=alert(document.domain)&gt; - passed')
end end
end end
end end
......
...@@ -57,25 +57,6 @@ describe('Job', () => { ...@@ -57,25 +57,6 @@ describe('Job', () => {
expect(job.buildStage).toBe('test'); expect(job.buildStage).toBe('test');
expect(job.state).toBe(''); expect(job.state).toBe('');
}); });
it('only shows the jobs matching the current stage', () => {
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
it('selects the current stage in the build dropdown menu', () => {
expect($('.stage-selection').text()).toBe('test');
});
it('updates the jobs when the build dropdown changes', () => {
$('.stage-item:contains("build")').click();
expect($('.stage-selection').text()).toBe('build');
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
}); });
describe('running build', () => { describe('running build', () => {
......
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import component from '~/jobs/components/jobs_container.vue'; import component from '~/jobs/components/jobs_container.vue';
import mountComponent from '../../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Artifacts block', () => { describe('Jobs List block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
let vm; let vm;
...@@ -16,8 +16,7 @@ describe('Artifacts block', () => { ...@@ -16,8 +16,7 @@ describe('Artifacts block', () => {
text: 'passed', text: 'passed',
tooltip: 'passed', tooltip: 'passed',
}, },
path: 'job/233432756', id: 233432756,
id: '233432756',
tooltip: 'build - passed', tooltip: 'build - passed',
retried: true, retried: true,
}; };
...@@ -33,8 +32,7 @@ describe('Artifacts block', () => { ...@@ -33,8 +32,7 @@ describe('Artifacts block', () => {
text: 'passed', text: 'passed',
tooltip: 'passed', tooltip: 'passed',
}, },
path: 'job/2322756', id: 2322756,
id: '2322756',
tooltip: 'build - passed', tooltip: 'build - passed',
active: true, active: true,
}; };
...@@ -50,8 +48,7 @@ describe('Artifacts block', () => { ...@@ -50,8 +48,7 @@ describe('Artifacts block', () => {
text: 'passed', text: 'passed',
tooltip: 'passed', tooltip: 'passed',
}, },
path: 'job/232153', id: 232153,
id: '232153',
tooltip: 'build - passed', tooltip: 'build - passed',
}; };
...@@ -62,14 +59,16 @@ describe('Artifacts block', () => { ...@@ -62,14 +59,16 @@ describe('Artifacts block', () => {
it('renders list of jobs', () => { it('renders list of jobs', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [job, retried, active], jobs: [job, retried, active],
jobId: 12313,
}); });
expect(vm.$el.querySelectorAll('a').length).toEqual(3); expect(vm.$el.querySelectorAll('a').length).toEqual(3);
}); });
it('renders arrow right when job is active', () => { it('renders arrow right when job id matches `jobId`', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [active], jobs: [active],
jobId: active.id,
}); });
expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull(); expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull();
...@@ -78,6 +77,7 @@ describe('Artifacts block', () => { ...@@ -78,6 +77,7 @@ describe('Artifacts block', () => {
it('does not render arrow right when job is not active', () => { it('does not render arrow right when job is not active', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [job], jobs: [job],
jobId: active.id,
}); });
expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull(); expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull();
...@@ -86,6 +86,7 @@ describe('Artifacts block', () => { ...@@ -86,6 +86,7 @@ describe('Artifacts block', () => {
it('renders job name when present', () => { it('renders job name when present', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [job], jobs: [job],
jobId: active.id,
}); });
expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name); expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name);
...@@ -95,6 +96,7 @@ describe('Artifacts block', () => { ...@@ -95,6 +96,7 @@ describe('Artifacts block', () => {
it('renders job id when job name is not available', () => { it('renders job id when job name is not available', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [retried], jobs: [retried],
jobId: active.id,
}); });
expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id); expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id);
...@@ -103,14 +105,16 @@ describe('Artifacts block', () => { ...@@ -103,14 +105,16 @@ describe('Artifacts block', () => {
it('links to the job page', () => { it('links to the job page', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [job], jobs: [job],
jobId: active.id,
}); });
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.path); expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.status.details_path);
}); });
it('renders retry icon when job was retried', () => { it('renders retry icon when job was retried', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [retried], jobs: [retried],
jobId: active.id,
}); });
expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull(); expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull();
...@@ -119,6 +123,7 @@ describe('Artifacts block', () => { ...@@ -119,6 +123,7 @@ describe('Artifacts block', () => {
it('does not render retry icon when job was not retried', () => { it('does not render retry icon when job was not retried', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
jobs: [job], jobs: [job],
jobId: active.id,
}); });
expect(vm.$el.querySelector('.js-retry-icon')).toBeNull(); expect(vm.$el.querySelector('.js-retry-icon')).toBeNull();
......
import Vue from 'vue'; import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
import job from '../mock_data'; import createStore from '~/jobs/store';
import mountComponent from '../../helpers/vue_mount_component_helper'; import job, { stages, jobsInStage } from '../mock_data';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/vue_component_helper';
describe('Sidebar details block', () => { describe('Sidebar details block', () => {
let SidebarComponent; const SidebarComponent = Vue.extend(sidebarDetailsBlock);
let vm; let vm;
let store;
function trimWhitespace(element) {
return element.textContent.replace(/\s+/g, ' ').trim();
}
beforeEach(() => { beforeEach(() => {
SidebarComponent = Vue.extend(sidebarDetailsBlock); store = createStore();
}); });
afterEach(() => { afterEach(() => {
...@@ -21,19 +20,21 @@ describe('Sidebar details block', () => { ...@@ -21,19 +20,21 @@ describe('Sidebar details block', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render a loading spinner', () => { it('should render a loading spinner', () => {
vm = mountComponent(SidebarComponent, { store.dispatch('requestJob');
job: {}, vm = mountComponentWithStore(SidebarComponent, { store });
isLoading: true,
});
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
}); });
}); });
describe('when there is no retry path retry', () => { describe('when there is no retry path retry', () => {
it('should not render a retry button', () => { it('should not render a retry button', () => {
vm = mountComponent(SidebarComponent, { const copy = Object.assign({}, job);
job: {}, delete copy.retry_path;
isLoading: false,
store.dispatch('receiveJobSuccess', copy);
vm = mountComponentWithStore(SidebarComponent, {
store,
}); });
expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
...@@ -42,10 +43,8 @@ describe('Sidebar details block', () => { ...@@ -42,10 +43,8 @@ describe('Sidebar details block', () => {
describe('without terminal path', () => { describe('without terminal path', () => {
it('does not render terminal link', () => { it('does not render terminal link', () => {
vm = mountComponent(SidebarComponent, { store.dispatch('receiveJobSuccess', job);
job, vm = mountComponentWithStore(SidebarComponent, { store });
isLoading: false,
});
expect(vm.$el.querySelector('.js-terminal-link')).toBeNull(); expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
}); });
...@@ -53,10 +52,12 @@ describe('Sidebar details block', () => { ...@@ -53,10 +52,12 @@ describe('Sidebar details block', () => {
describe('with terminal path', () => { describe('with terminal path', () => {
it('renders terminal link', () => { it('renders terminal link', () => {
vm = mountComponent(SidebarComponent, { store.dispatch('receiveJobSuccess', job);
job, vm = mountComponentWithStore(SidebarComponent, {
isLoading: false, store,
props: {
terminalPath: 'job/43123/terminal', terminalPath: 'job/43123/terminal',
},
}); });
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull(); expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
...@@ -64,10 +65,8 @@ describe('Sidebar details block', () => { ...@@ -64,10 +65,8 @@ describe('Sidebar details block', () => {
}); });
beforeEach(() => { beforeEach(() => {
vm = mountComponent(SidebarComponent, { store.dispatch('receiveJobSuccess', job);
job, vm = mountComponentWithStore(SidebarComponent, { store });
isLoading: false,
});
}); });
describe('actions', () => { describe('actions', () => {
...@@ -89,7 +88,7 @@ describe('Sidebar details block', () => { ...@@ -89,7 +88,7 @@ describe('Sidebar details block', () => {
describe('information', () => { describe('information', () => {
it('should render merge request link', () => { it('should render merge request link', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2'); expect(trimText(vm.$el.querySelector('.js-job-mr').textContent)).toEqual('Merge Request: !2');
expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual( expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
job.merge_request.path, job.merge_request.path,
...@@ -97,43 +96,101 @@ describe('Sidebar details block', () => { ...@@ -97,43 +96,101 @@ describe('Sidebar details block', () => {
}); });
it('should render job duration', () => { it('should render job duration', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual( expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual(
'Duration: 6 seconds', 'Duration: 6 seconds',
); );
}); });
it('should render erased date', () => { it('should render erased date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago'); expect(trimText(vm.$el.querySelector('.js-job-erased').textContent)).toEqual(
'Erased: 3 weeks ago',
);
}); });
it('should render finished date', () => { it('should render finished date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual( expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual(
'Finished: 3 weeks ago', 'Finished: 3 weeks ago',
); );
}); });
it('should render queued date', () => { it('should render queued date', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds'); expect(trimText(vm.$el.querySelector('.js-job-queued').textContent)).toEqual(
'Queued: 9 seconds',
);
}); });
it('should render runner ID', () => { it('should render runner ID', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual( expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual(
'Runner: local ci runner (#1)', 'Runner: local ci runner (#1)',
); );
}); });
it('should render timeout information', () => { it('should render timeout information', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual( expect(trimText(vm.$el.querySelector('.js-job-timeout').textContent)).toEqual(
'Timeout: 1m 40s (from runner)', 'Timeout: 1m 40s (from runner)',
); );
}); });
it('should render coverage', () => { it('should render coverage', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%'); expect(trimText(vm.$el.querySelector('.js-job-coverage').textContent)).toEqual(
'Coverage: 20%',
);
}); });
it('should render tags', () => { it('should render tags', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag'); expect(trimText(vm.$el.querySelector('.js-job-tags').textContent)).toEqual('Tags: tag');
});
});
describe('stages dropdown', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
});
describe('while fetching stages', () => {
it('renders dropdown with More label', () => {
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More');
});
});
describe('with stages', () => {
beforeEach(() => {
store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('renders first stage as selected', () => {
expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual(
stages[0].name,
);
});
});
describe('without jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('does not render job container', () => {
expect(vm.$el.querySelector('.js-jobs-container')).toBeNull();
});
});
describe('with jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
store.dispatch('receiveStagesSuccess', stages);
store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
vm = mountComponentWithStore(SidebarComponent, { store });
});
it('renders list of jobs', () => {
expect(vm.$el.querySelector('.js-jobs-container')).not.toBeNull();
});
}); });
}); });
}); });
...@@ -8,19 +8,10 @@ describe('Artifacts block', () => { ...@@ -8,19 +8,10 @@ describe('Artifacts block', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
pipelineId: 28029444, pipeline: {
pipelinePath: 'pipeline/28029444', id: 28029444,
pipelineRef: '50101-truncated-job-information', details: {
pipelineRefPath: 'commits/50101-truncated-job-information', status: {
stages: [
{
name: 'build',
},
{
name: 'test',
},
],
pipelineStatus: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
group: 'success', group: 'success',
has_details: true, has_details: true,
...@@ -29,6 +20,21 @@ describe('Artifacts block', () => { ...@@ -29,6 +20,21 @@ describe('Artifacts block', () => {
text: 'passed', text: 'passed',
tooltip: 'passed', tooltip: 'passed',
}, },
},
path: 'pipeline/28029444',
},
ref: {
path: 'commits/50101-truncated-job-information',
name: '50101-truncated-job-information',
},
stages: [
{
name: 'build',
},
{
name: 'test',
},
],
}); });
}); });
......
This diff is collapsed.
...@@ -27,7 +27,6 @@ import { ...@@ -27,7 +27,6 @@ import {
receiveStagesSuccess, receiveStagesSuccess,
receiveStagesError, receiveStagesError,
requestJobsForStage, requestJobsForStage,
setSelectedStage,
fetchJobsForStage, fetchJobsForStage,
receiveJobsForStageSuccess, receiveJobsForStageSuccess,
receiveJobsForStageError, receiveJobsForStageError,
...@@ -236,7 +235,8 @@ describe('Job State actions', () => { ...@@ -236,7 +235,8 @@ describe('Job State actions', () => {
}, },
{ {
payload: { payload: {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :', complete: true, html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
}, },
type: 'receiveTraceSuccess', type: 'receiveTraceSuccess',
}, },
...@@ -421,7 +421,9 @@ describe('Job State actions', () => { ...@@ -421,7 +421,9 @@ describe('Job State actions', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
mockedState.stagesEndpoint = `${TEST_HOST}/endpoint.json`; mockedState.job.pipeline = {
path: `${TEST_HOST}/endpoint.json/stages`,
};
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -430,8 +432,10 @@ describe('Job State actions', () => { ...@@ -430,8 +432,10 @@ describe('Job State actions', () => {
}); });
describe('success', () => { describe('success', () => {
it('dispatches requestStages and receiveStagesSuccess ', done => { it('dispatches requestStages and receiveStagesSuccess, fetchJobsForStage ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]); mock
.onGet(`${TEST_HOST}/endpoint.json/stages`)
.replyOnce(200, { details: { stages: [{ id: 121212, name: 'build' }] } });
testAction( testAction(
fetchStages, fetchStages,
...@@ -446,6 +450,10 @@ describe('Job State actions', () => { ...@@ -446,6 +450,10 @@ describe('Job State actions', () => {
payload: [{ id: 121212, name: 'build' }], payload: [{ id: 121212, name: 'build' }],
type: 'receiveStagesSuccess', type: 'receiveStagesSuccess',
}, },
{
payload: { id: 121212, name: 'build' },
type: 'fetchJobsForStage',
},
], ],
done, done,
); );
...@@ -516,24 +524,10 @@ describe('Job State actions', () => { ...@@ -516,24 +524,10 @@ describe('Job State actions', () => {
}); });
}); });
describe('setSelectedStage', () => {
it('should commit SET_SELECTED_STAGE mutation ', done => {
testAction(
setSelectedStage,
{ name: 'build' },
mockedState,
[{ type: types.SET_SELECTED_STAGE, payload: { name: 'build' } }],
[],
done,
);
});
});
describe('fetchJobsForStage', () => { describe('fetchJobsForStage', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
mockedState.stageJobsEndpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -542,19 +536,17 @@ describe('Job State actions', () => { ...@@ -542,19 +536,17 @@ describe('Job State actions', () => {
}); });
describe('success', () => { describe('success', () => {
it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageSuccess ', done => { it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]); mock
.onGet(`${TEST_HOST}/jobs.json`)
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
testAction( testAction(
fetchJobsForStage, fetchJobsForStage,
null, { dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState, mockedState,
[], [],
[ [
{
type: 'setSelectedStage',
payload: null,
},
{ {
type: 'requestJobsForStage', type: 'requestJobsForStage',
}, },
...@@ -570,20 +562,16 @@ describe('Job State actions', () => { ...@@ -570,20 +562,16 @@ describe('Job State actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
}); });
it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageError', done => { it('dispatches requestJobsForStage and receiveJobsForStageError', done => {
testAction( testAction(
fetchJobsForStage, fetchJobsForStage,
null, { dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState, mockedState,
[], [],
[ [
{
payload: null,
type: 'setSelectedStage',
},
{ {
type: 'requestJobsForStage', type: 'requestJobsForStage',
}, },
......
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