Commit cf26610f authored by Phil Hughes's avatar Phil Hughes
Browse files

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 {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -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
......@@ -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';
......@@ -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" />
......
<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';
......@@ -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,
......@@ -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}%`;
},
......@@ -97,20 +92,31 @@
},
hasStages() {
return (
this.job &&
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0
) || false;
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script>
<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">
<strong class="inline prepend-top-8">
{{ job.name }}
......@@ -137,7 +143,8 @@
<button
:aria-label="__('Toggle Sidebar')"
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
aria-hidden="true"
......@@ -146,7 +153,6 @@
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
......@@ -168,7 +174,7 @@
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
<div :class="{ block : renderBlock }">
<p
v-if="job.merge_request"
class="build-detail-row js-job-mr"
......@@ -266,11 +272,26 @@
:commit="commit"
:merge-request="job.merge_request"
/>
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template>
<gl-loading-icon
v-if="isLoading"
v-else
:size="2"
class="prepend-top-10"
/>
</div>
<jobs-container
v-if="!isLoading && jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
</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: {
......@@ -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 {
......@@ -41,51 +25,68 @@
};
},
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"
/>
<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
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
>
{{ selectedStage }}
<icon name="chevron-down" />
<i class="fa fa-chevron-down" ></i>
</button>
<ul class="dropdown-menu">
<li
v-for="(stage, index) in stages"
:key="index"
v-for="stage in stages"
:key="stage.name"
>
<button
type="button"
class="stage-item"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
......@@ -93,5 +94,4 @@
</li>
</ul>
</div>
</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 () => {
......@@ -13,6 +14,7 @@ export default () => {
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
......@@ -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,
},
......
......@@ -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.'));
......@@ -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) =>
......@@ -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) =>
......
......@@ -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;
......@@ -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 {
......
%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 @@
- 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 }
......
......@@ -4310,9 +4310,6 @@ msgstr ""
msgid "Pipeline"
msgstr ""
msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
msgstr ""
msgid "Pipeline Health"
msgstr ""
......@@ -7039,6 +7036,9 @@ msgstr ""
msgid "for this project"
msgstr ""
msgid "from"
msgstr ""
msgid "here"
msgstr ""
......
......@@ -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