Skip to content
Snippets Groups Projects
Commit f72a1bf0 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes
Browse files

Moves stages dropdown into the new vue app

parent c4d9f402
No related branches found
No related tags found
No related merge requests found
Showing
with 1727 additions and 259 deletions
Loading
Loading
@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
 
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
Loading
Loading
@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout);
 
this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
 
this.$document
.off('click', '.js-sidebar-build-toggle')
.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.$window
Loading
Loading
@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
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>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
Loading
Loading
@@ -16,26 +17,39 @@
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
},
};
</script>
<template>
<div class="builds-container">
<div class="js-jobs-container builds-container">
<div
v-for="job in jobs"
:key="job.id"
class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
>
<a
v-for="job in jobs"
:key="job.id"
v-tooltip
:href="job.path"
:title="job.tooltip"
:class="{ active: job.active, retried: job.retried }"
:href="job.status.details_path"
:title="tooltipText(job)"
data-container="body"
>
<icon
v-if="job.active"
v-if="isJobActive(job.id)"
name="arrow-right"
class="js-arrow-right"
class="js-arrow-right icon-arrow-right"
/>
 
<ci-icon :status="job.status" />
Loading
Loading
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
Loading
Loading
@@ -7,26 +8,22 @@
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
 
export default {
name: 'SidebarDetailsBlock',
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
Loading
Loading
@@ -39,9 +36,7 @@
},
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
...mapState(['job', 'isLoading', 'stages', 'jobs']),
coverage() {
return `${this.job.coverage}%`;
},
Loading
Loading
@@ -97,180 +92,206 @@
},
hasStages() {
return (
this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0
) || false;
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script>
<template>
<div>
<div class="block">
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
<a
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<a
v-if="terminalPath"
:href="terminalPath"
class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }}
<icon name="external-link" />
</a>
<button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
>
<a
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
>
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
</a>
</p>
<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">
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
<a
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<a
v-if="terminalPath"
:href="terminalPath"
class="js-terminal-link pull-right btn btn-primary
btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }}
<icon name="external-link" />
</a>
<button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
>
<a
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
>
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
</div>
<div :class="{ block : renderBlock }">
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
>
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
</a>
</p>
 
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormated(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormated(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row
v-if="job.queued"
:value="queued"
class="js-job-queued"
title="Queued"
/>
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row
v-if="job.runner"
:value="runnerId"
class="js-job-runner"
title="Runner"
/>
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p
v-if="job.tags.length"
class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
:key="i"
class="label label-primary">
{{ tag }}
</span>
</p>
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormated(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormated(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row
v-if="job.queued"
:value="queued"
class="js-job-queued"
title="Queued"
/>
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row
v-if="job.runner"
:value="runnerId"
class="js-job-runner"
title="Runner"
/>
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p
v-if="job.tags.length"
class="build-detail-row js-job-tags"
>
<span class="build-light-text">
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
:key="i"
class="label label-primary">
{{ tag }}
</span>
</p>
 
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a
:href="job.cancel_path"
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
{{ __('Cancel') }}
</a>
</div>
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a
:href="job.cancel_path"
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
{{ __('Cancel') }}
</a>
</div>
</div>
<artifacts-block
v-if="hasArtifact"
:artifact="job.artifact"
/>
<trigger-block
v-if="hasTriggers"
:trigger="job.trigger"
/>
<commit-block
:is-last-block="hasStages"
:commit="commit"
:merge-request="job.merge_request"
/>
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template>
<gl-loading-icon
v-else
:size="2"
class="prepend-top-10"
/>
</div>
<artifacts-block
v-if="hasArtifact"
:artifact="job.artifact"
/>
<trigger-block
v-if="hasTriggers"
:trigger="job.trigger"
/>
<commit-block
:is-last-block="hasStages"
:commit="commit"
:merge-request="job.merge_request"
<jobs-container
v-if="!isLoading && jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
</template>
<gl-loading-icon
v-if="isLoading"
:size="2"
class="prepend-top-10"
/>
</div>
</div>
</aside>
</template>
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { sprintf, __ } from '~/locale';
import { __ } from '~/locale';
 
export default {
components: {
Loading
Loading
@@ -10,30 +10,14 @@
Icon,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
},
data() {
return {
Loading
Loading
@@ -41,57 +25,73 @@
};
},
computed: {
pipelineLink() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
pipelineId: this.pipelineId,
pipelineLinkEnd: '</a>',
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
pipelineRef: this.pipelineRef,
pipelineLinkRefEnd: '</a>',
}, false);
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
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.selectedStage = stage.name;
},
},
};
</script>
<template>
<div class="block-last">
<ci-icon :status="pipelineStatus" />
<div class="block-last dropdown">
<ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
{{ __('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>
 
<p v-html="pipelineLink"></p>
<button
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
>
{{ selectedStage }}
<i class="fa fa-chevron-down" ></i>
</button>
 
<div class="dropdown">
<button
type="button"
data-toggle="dropdown"
<ul class="dropdown-menu">
<li
v-for="stage in stages"
:key="stage.name"
>
{{ selectedStage }}
<icon name="chevron-down" />
</button>
<ul class="dropdown-menu">
<li
v-for="(stage, index) in stages"
:key="index"
<button
type="button"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
<button
type="button"
class="stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
</button>
</li>
</ul>
</div>
{{ stage.name }}
</button>
</li>
</ul>
</div>
</template>
import { mapState } from 'vuex';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobHeader from './components/header.vue';
import DetailsBlock from './components/sidebar_details_block.vue';
import Sidebar from './components/sidebar.vue';
import createStore from './store';
 
export default () => {
Loading
Loading
@@ -13,6 +14,7 @@ export default () => {
 
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
 
// Header
Loading
Loading
@@ -43,17 +45,25 @@ export default () => {
new Vue({
el: detailsBlockElement,
components: {
DetailsBlock,
Sidebar,
},
store,
computed: {
...mapState(['job', 'isLoading']),
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('details-block', {
return createElement('sidebar', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
Loading
Loading
Loading
Loading
@@ -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 }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
Loading
Loading
@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
 
axios
.get(state.stagesEndpoint)
.then(({ data }) => dispatch('receiveStagesSuccess', data))
.get(state.job.pipeline.path)
.then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError'));
};
export const receiveStagesSuccess = ({ commit }, data) =>
Loading
Loading
@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown
*/
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
export const fetchJobsForStage = ({ state, dispatch }, stage) => {
dispatch('setSelectedStage', stage);
export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('requestJobsForStage');
 
axios
.get(state.stageJobsEndpoint)
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data))
.get(stage.dropdown_path, {
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'));
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
Loading
Loading
Loading
Loading
@@ -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 {
background-color: $white-light;
border-top: 1px solid $border-color;
Loading
Loading
@@ -381,15 +364,11 @@
position: absolute;
left: 15px;
top: 20px;
display: none;
display: block;
}
 
&.active {
font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
}
 
&.retried {
Loading
Loading
%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')
Loading
Loading
@@ -93,7 +93,7 @@
- else
= 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 }
 
Loading
Loading
Loading
Loading
@@ -4310,9 +4310,6 @@ msgstr ""
msgid "Pipeline"
msgstr ""
 
msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
msgstr ""
msgid "Pipeline Health"
msgstr ""
 
Loading
Loading
@@ -7039,6 +7036,9 @@ msgstr ""
msgid "for this project"
msgstr ""
 
msgid "from"
msgstr ""
msgid "here"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -60,7 +60,7 @@
context 'with manual action' do
let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
name: 'deploy to production')
name: 'deploy to production', environment: environment.name)
end
 
context 'when user has ability to trigger deployment' do
Loading
Loading
@@ -73,12 +73,16 @@
expect(page).to have_link(action.name.humanize)
end
 
it 'does allow to play manual action' do
it 'does allow to play manual action', :js do
expect(action).to be_manual
 
find('button.dropdown').click
expect { click_link(action.name.humanize) }
.not_to change { Ci::Pipeline.count }
 
wait_for_all_requests
expect(page).to have_content(action.name)
expect(action.reload).to be_pending
end
Loading
Loading
@@ -165,10 +169,10 @@
name: action.ref, project: project)
end
 
it 'allows to stop environment' do
it 'allows to stop environment', :js do
click_button('Stop')
click_button('Stop environment') # confirm modal
wait_for_all_requests
expect(page).to have_content('close_app')
end
end
Loading
Loading
Loading
Loading
@@ -38,9 +38,10 @@
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
 
it 'displays the failure reason' do
wait_for_all_requests
within('.builds-container') do
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
Loading
Loading
@@ -49,9 +50,10 @@
let!(:build) { create(:ci_build, :failed, :retried, :trace_artifact, pipeline: pipeline) }
 
it 'displays the failure reason and retried label' do
wait_for_all_requests
within('.builds-container') do
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
Loading
Loading
Loading
Loading
@@ -134,23 +134,25 @@
expect(page).to have_content pipeline.commit.title
end
 
it 'shows active job' do
it 'shows active job', :js do
visit project_job_path(project, job)
 
wait_for_requests
expect(page).to have_selector('.build-job.active')
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)>') }
 
before do
visit project_job_path(project, job)
wait_for_requests
end
 
it 'renders escaped tooltip name' 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
Loading
Loading
Loading
Loading
@@ -57,25 +57,6 @@ describe('Job', () => {
expect(job.buildStage).toBe('test');
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', () => {
Loading
Loading
Loading
Loading
@@ -2,7 +2,7 @@ import Vue from 'vue';
import component from '~/jobs/components/jobs_container.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
 
describe('Artifacts block', () => {
describe('Jobs List block', () => {
const Component = Vue.extend(component);
let vm;
 
Loading
Loading
@@ -16,8 +16,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/233432756',
id: '233432756',
id: 233432756,
tooltip: 'build - passed',
retried: true,
};
Loading
Loading
@@ -33,8 +32,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/2322756',
id: '2322756',
id: 2322756,
tooltip: 'build - passed',
active: true,
};
Loading
Loading
@@ -50,8 +48,7 @@ describe('Artifacts block', () => {
text: 'passed',
tooltip: 'passed',
},
path: 'job/232153',
id: '232153',
id: 232153,
tooltip: 'build - passed',
};
 
Loading
Loading
@@ -62,14 +59,16 @@ describe('Artifacts block', () => {
it('renders list of jobs', () => {
vm = mountComponent(Component, {
jobs: [job, retried, active],
jobId: 12313,
});
 
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, {
jobs: [active],
jobId: active.id,
});
 
expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull();
Loading
Loading
@@ -78,6 +77,7 @@ describe('Artifacts block', () => {
it('does not render arrow right when job is not active', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
 
expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull();
Loading
Loading
@@ -86,6 +86,7 @@ describe('Artifacts block', () => {
it('renders job name when present', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
 
expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name);
Loading
Loading
@@ -95,6 +96,7 @@ describe('Artifacts block', () => {
it('renders job id when job name is not available', () => {
vm = mountComponent(Component, {
jobs: [retried],
jobId: active.id,
});
 
expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id);
Loading
Loading
@@ -103,14 +105,16 @@ describe('Artifacts block', () => {
it('links to the job page', () => {
vm = mountComponent(Component, {
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', () => {
vm = mountComponent(Component, {
jobs: [retried],
jobId: active.id,
});
 
expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull();
Loading
Loading
@@ -119,6 +123,7 @@ describe('Artifacts block', () => {
it('does not render retry icon when job was not retried', () => {
vm = mountComponent(Component, {
jobs: [job],
jobId: active.id,
});
 
expect(vm.$el.querySelector('.js-retry-icon')).toBeNull();
Loading
Loading
import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
import job from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
import createStore from '~/jobs/store';
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', () => {
let SidebarComponent;
const SidebarComponent = Vue.extend(sidebarDetailsBlock);
let vm;
function trimWhitespace(element) {
return element.textContent.replace(/\s+/g, ' ').trim();
}
let store;
 
beforeEach(() => {
SidebarComponent = Vue.extend(sidebarDetailsBlock);
store = createStore();
});
 
afterEach(() => {
Loading
Loading
@@ -21,19 +20,21 @@ describe('Sidebar details block', () => {
 
describe('when it is loading', () => {
it('should render a loading spinner', () => {
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: true,
});
store.dispatch('requestJob');
vm = mountComponentWithStore(SidebarComponent, { store });
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
});
});
 
describe('when there is no retry path retry', () => {
it('should not render a retry button', () => {
vm = mountComponent(SidebarComponent, {
job: {},
isLoading: false,
const copy = Object.assign({}, job);
delete copy.retry_path;
store.dispatch('receiveJobSuccess', copy);
vm = mountComponentWithStore(SidebarComponent, {
store,
});
 
expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
Loading
Loading
@@ -42,10 +43,8 @@ describe('Sidebar details block', () => {
 
describe('without terminal path', () => {
it('does not render terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, { store });
 
expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
});
Loading
Loading
@@ -53,10 +52,12 @@ describe('Sidebar details block', () => {
 
describe('with terminal path', () => {
it('renders terminal link', () => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
terminalPath: 'job/43123/terminal',
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, {
store,
props: {
terminalPath: 'job/43123/terminal',
},
});
 
expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
Loading
Loading
@@ -64,10 +65,8 @@ describe('Sidebar details block', () => {
});
 
beforeEach(() => {
vm = mountComponent(SidebarComponent, {
job,
isLoading: false,
});
store.dispatch('receiveJobSuccess', job);
vm = mountComponentWithStore(SidebarComponent, { store });
});
 
describe('actions', () => {
Loading
Loading
@@ -89,7 +88,7 @@ describe('Sidebar details block', () => {
 
describe('information', () => {
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(
job.merge_request.path,
Loading
Loading
@@ -97,43 +96,101 @@ describe('Sidebar details block', () => {
});
 
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',
);
});
 
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', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual(
'Finished: 3 weeks ago',
);
});
 
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', () => {
expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual(
expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual(
'Runner: local ci runner (#1)',
);
});
 
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)',
);
});
 
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', () => {
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();
});
});
});
});
Loading
Loading
@@ -8,10 +8,25 @@ describe('Artifacts block', () => {
 
beforeEach(() => {
vm = mountComponent(Component, {
pipelineId: 28029444,
pipelinePath: 'pipeline/28029444',
pipelineRef: '50101-truncated-job-information',
pipelineRefPath: 'commits/50101-truncated-job-information',
pipeline: {
id: 28029444,
details: {
status: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
group: 'success',
has_details: true,
icon: 'status_success',
label: 'passed',
text: 'passed',
tooltip: 'passed',
},
},
path: 'pipeline/28029444',
},
ref: {
path: 'commits/50101-truncated-job-information',
name: '50101-truncated-job-information',
},
stages: [
{
name: 'build',
Loading
Loading
@@ -20,15 +35,6 @@ describe('Artifacts block', () => {
name: 'test',
},
],
pipelineStatus: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
group: 'success',
has_details: true,
icon: 'status_success',
label: 'passed',
text: 'passed',
tooltip: 'passed',
},
});
});
 
Loading
Loading
This diff is collapsed.
Loading
Loading
@@ -27,7 +27,6 @@ import {
receiveStagesSuccess,
receiveStagesError,
requestJobsForStage,
setSelectedStage,
fetchJobsForStage,
receiveJobsForStageSuccess,
receiveJobsForStageError,
Loading
Loading
@@ -236,7 +235,8 @@ describe('Job State actions', () => {
},
{
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',
},
Loading
Loading
@@ -421,7 +421,9 @@ describe('Job State actions', () => {
let mock;
 
beforeEach(() => {
mockedState.stagesEndpoint = `${TEST_HOST}/endpoint.json`;
mockedState.job.pipeline = {
path: `${TEST_HOST}/endpoint.json/stages`,
};
mock = new MockAdapter(axios);
});
 
Loading
Loading
@@ -430,8 +432,10 @@ describe('Job State actions', () => {
});
 
describe('success', () => {
it('dispatches requestStages and receiveStagesSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]);
it('dispatches requestStages and receiveStagesSuccess, fetchJobsForStage ', done => {
mock
.onGet(`${TEST_HOST}/endpoint.json/stages`)
.replyOnce(200, { details: { stages: [{ id: 121212, name: 'build' }] } });
 
testAction(
fetchStages,
Loading
Loading
@@ -446,6 +450,10 @@ describe('Job State actions', () => {
payload: [{ id: 121212, name: 'build' }],
type: 'receiveStagesSuccess',
},
{
payload: { id: 121212, name: 'build' },
type: 'fetchJobsForStage',
},
],
done,
);
Loading
Loading
@@ -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', () => {
let mock;
 
beforeEach(() => {
mockedState.stageJobsEndpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
 
Loading
Loading
@@ -542,19 +536,17 @@ describe('Job State actions', () => {
});
 
describe('success', () => {
it('dispatches setSelectedStage, requestJobsForStage and receiveJobsForStageSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [{ id: 121212, name: 'build' }]);
it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
mock
.onGet(`${TEST_HOST}/jobs.json`)
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
 
testAction(
fetchJobsForStage,
null,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
[],
[
{
type: 'setSelectedStage',
payload: null,
},
{
type: 'requestJobsForStage',
},
Loading
Loading
@@ -570,20 +562,16 @@ describe('Job State actions', () => {
 
describe('error', () => {
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(
fetchJobsForStage,
null,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
[],
[
{
payload: null,
type: 'setSelectedStage',
},
{
type: 'requestJobsForStage',
},
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment