Commit 04cc135e authored by Fabio Huser's avatar Fabio Huser Committed by Paul Slaughter

Replace pipeline custom action array header with slot and buttons

Closes #195352

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22839
parent 36a8c8fe
<script> <script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui'; import { GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '~/vue_shared/components/header_ci_component.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -12,6 +13,10 @@ export default { ...@@ -12,6 +13,10 @@ export default {
ciHeader, ciHeader,
GlLoadingIcon, GlLoadingIcon,
GlModal, GlModal,
LoadingButton,
},
directives: {
GlModal: GlModalDirective,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -25,7 +30,9 @@ export default { ...@@ -25,7 +30,9 @@ export default {
}, },
data() { data() {
return { return {
actions: this.getActions(), isCanceling: false,
isRetrying: false,
isDeleting: false,
}; };
}, },
...@@ -43,67 +50,18 @@ export default { ...@@ -43,67 +50,18 @@ export default {
}, },
}, },
watch: {
pipeline() {
this.actions = this.getActions();
},
},
methods: { methods: {
onActionClicked(action) { cancelPipeline() {
if (action.modal) { this.isCanceling = true;
this.$root.$emit('bv::show::modal', action.modal); eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
} else {
this.postAction(action);
}
}, },
postAction(action) { retryPipeline() {
const index = this.actions.indexOf(action); this.isRetrying = true;
eventHub.$emit('headerPostAction', this.pipeline.retry_path);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerPostAction', action);
}, },
deletePipeline() { deletePipeline() {
const index = this.actions.findIndex(action => action.modal === DELETE_MODAL_ID); this.isDeleting = true;
eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerDeleteAction', this.actions[index]);
},
getActions() {
const actions = [];
if (this.pipeline.retry_path) {
actions.push({
label: __('Retry'),
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
isLoading: false,
});
}
if (this.pipeline.cancel_path) {
actions.push({
label: __('Cancel running'),
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
isLoading: false,
});
}
if (this.pipeline.delete_path) {
actions.push({
label: __('Delete'),
path: this.pipeline.delete_path,
modal: DELETE_MODAL_ID,
cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted',
isLoading: false,
});
}
return actions;
}, },
}, },
DELETE_MODAL_ID, DELETE_MODAL_ID,
...@@ -117,10 +75,38 @@ export default { ...@@ -117,10 +75,38 @@ export default {
:item-id="pipeline.id" :item-id="pipeline.id"
:time="pipeline.created_at" :time="pipeline.created_at"
:user="pipeline.user" :user="pipeline.user"
:actions="actions"
item-name="Pipeline" item-name="Pipeline"
@actionClicked="onActionClicked" >
<loading-button
v-if="pipeline.retry_path"
:loading="isRetrying"
:disabled="isRetrying"
class="js-retry-button btn btn-inverted-secondary"
container-class="d-inline"
:label="__('Retry')"
@click="retryPipeline()"
/>
<loading-button
v-if="pipeline.cancel_path"
:loading="isCanceling"
:disabled="isCanceling"
class="js-btn-cancel-pipeline btn btn-danger"
container-class="d-inline"
:label="__('Cancel running')"
@click="cancelPipeline()"
/>
<loading-button
v-if="pipeline.delete_path"
v-gl-modal="$options.DELETE_MODAL_ID"
:loading="isDeleting"
:disabled="isDeleting"
class="js-btn-delete-pipeline btn btn-danger btn-inverted"
container-class="d-inline"
:label="__('Delete')"
/> />
</ci-header>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" /> <gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
......
...@@ -70,16 +70,16 @@ export default () => { ...@@ -70,16 +70,16 @@ export default () => {
eventHub.$off('headerDeleteAction', this.deleteAction); eventHub.$off('headerDeleteAction', this.deleteAction);
}, },
methods: { methods: {
postAction(action) { postAction(path) {
this.mediator.service this.mediator.service
.postAction(action.path) .postAction(path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
deleteAction(action) { deleteAction(path) {
this.mediator.stopPipelinePoll(); this.mediator.stopPipelinePoll();
this.mediator.service this.mediator.service
.deleteAction(action.path) .deleteAction(path)
.then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success'))) .then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
.catch(() => Flash(__('An error occurred while deleting the pipeline.'))); .catch(() => Flash(__('An error occurred while deleting the pipeline.')));
}, },
......
...@@ -4,7 +4,6 @@ import { __, sprintf } from '~/locale'; ...@@ -4,7 +4,6 @@ import { __, sprintf } from '~/locale';
import CiIconBadge from './ci_badge_link.vue'; import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue'; import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
...@@ -20,7 +19,6 @@ export default { ...@@ -20,7 +19,6 @@ export default {
UserAvatarImage, UserAvatarImage,
GlLink, GlLink,
GlButton, GlButton,
LoadingButton,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -47,11 +45,6 @@ export default { ...@@ -47,11 +45,6 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: { hasSidebarButton: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -71,9 +64,6 @@ export default { ...@@ -71,9 +64,6 @@ export default {
}, },
methods: { methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
onClickSidebarButton() { onClickSidebarButton() {
this.$emit('clickedSidebarButton'); this.$emit('clickedSidebarButton');
}, },
...@@ -115,18 +105,8 @@ export default { ...@@ -115,18 +105,8 @@ export default {
</template> </template>
</section> </section>
<section v-if="actions.length" class="header-action-buttons"> <section v-if="$slots.default" class="header-action-buttons">
<template v-for="(action, i) in actions"> <slot></slot>
<loading-button
:key="i"
:loading="action.isLoading"
:disabled="action.isLoading"
:class="action.cssClass"
container-class="d-inline"
:label="action.label"
@click="onClickAction(action)"
/>
</template>
</section> </section>
<gl-button <gl-button
v-if="hasSidebarButton" v-if="hasSidebarButton"
......
---
title: Replace custom action array in CI header bar with <slot>
merge_request: 22839
author: Fabio Huser
type: other
...@@ -8,6 +8,7 @@ describe('Pipeline details header', () => { ...@@ -8,6 +8,7 @@ describe('Pipeline details header', () => {
let props; let props;
beforeEach(() => { beforeEach(() => {
spyOn(eventHub, '$emit');
HeaderComponent = Vue.extend(headerComponent); HeaderComponent = Vue.extend(headerComponent);
const threeWeeksAgo = new Date(); const threeWeeksAgo = new Date();
...@@ -33,8 +34,9 @@ describe('Pipeline details header', () => { ...@@ -33,8 +34,9 @@ describe('Pipeline details header', () => {
email: 'foo@bar.com', email: 'foo@bar.com',
avatar_url: 'link', avatar_url: 'link',
}, },
retry_path: 'path', retry_path: 'retry',
delete_path: 'path', cancel_path: 'cancel',
delete_path: 'delete',
}, },
isLoading: false, isLoading: false,
}; };
...@@ -43,9 +45,14 @@ describe('Pipeline details header', () => { ...@@ -43,9 +45,14 @@ describe('Pipeline details header', () => {
}); });
afterEach(() => { afterEach(() => {
eventHub.$off();
vm.$destroy(); vm.$destroy();
}); });
const findDeleteModal = () => document.getElementById(headerComponent.DELETE_MODAL_ID);
const findDeleteModalSubmit = () =>
[...findDeleteModal().querySelectorAll('.btn')].find(x => x.textContent === 'Delete pipeline');
it('should render provided pipeline info', () => { it('should render provided pipeline info', () => {
expect( expect(
vm.$el vm.$el
...@@ -56,22 +63,46 @@ describe('Pipeline details header', () => { ...@@ -56,22 +63,46 @@ describe('Pipeline details header', () => {
}); });
describe('action buttons', () => { describe('action buttons', () => {
it('should call postAction when retry button action is clicked', done => { it('should not trigger eventHub when nothing happens', () => {
eventHub.$on('headerPostAction', action => { expect(eventHub.$emit).not.toHaveBeenCalled();
expect(action.path).toEqual('path');
done();
}); });
it('should call postAction when retry button action is clicked', () => {
vm.$el.querySelector('.js-retry-button').click(); vm.$el.querySelector('.js-retry-button').click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
});
it('should call postAction when cancel button action is clicked', () => {
vm.$el.querySelector('.js-btn-cancel-pipeline').click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
}); });
it('should fire modal event when delete button action is clicked', done => { it('does not show delete modal', () => {
vm.$root.$on('bv::modal::show', action => { expect(findDeleteModal()).not.toBeVisible();
expect(action.componentId).toEqual('pipeline-delete-modal');
done();
}); });
describe('when delete button action is clicked', () => {
beforeEach(done => {
vm.$el.querySelector('.js-btn-delete-pipeline').click(); vm.$el.querySelector('.js-btn-delete-pipeline').click();
// Modal needs two ticks to show
vm.$nextTick()
.then(() => vm.$nextTick())
.then(done)
.catch(done.fail);
});
it('should show delete modal', () => {
expect(findDeleteModal()).toBeVisible();
});
it('should call delete when modal is submitted', () => {
findDeleteModalSubmit().click();
expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
});
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
import headerCi from '~/vue_shared/components/header_ci_component.vue'; import headerCi from '~/vue_shared/components/header_ci_component.vue';
describe('Header CI Component', () => { describe('Header CI Component', () => {
...@@ -27,14 +27,6 @@ describe('Header CI Component', () => { ...@@ -27,14 +27,6 @@ describe('Header CI Component', () => {
email: 'foo@bar.com', email: 'foo@bar.com',
avatar_url: 'link', avatar_url: 'link',
}, },
actions: [
{
label: 'Retry',
path: 'path',
cssClass: 'btn',
isLoading: false,
},
],
hasSidebarButton: true, hasSidebarButton: true,
}; };
}); });
...@@ -43,6 +35,8 @@ describe('Header CI Component', () => { ...@@ -43,6 +35,8 @@ describe('Header CI Component', () => {
vm.$destroy(); vm.$destroy();
}); });
const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
describe('render', () => { describe('render', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(HeaderCi, props); vm = mountComponent(HeaderCi, props);
...@@ -68,24 +62,23 @@ describe('Header CI Component', () => { ...@@ -68,24 +62,23 @@ describe('Header CI Component', () => {
expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name); expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
}); });
it('should render provided actions', () => { it('should render sidebar toggle button', () => {
const btn = vm.$el.querySelector('.btn'); expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
expect(btn.tagName).toEqual('BUTTON');
expect(btn.textContent.trim()).toEqual(props.actions[0].label);
}); });
it('should show loading icon', done => { it('should not render header action buttons when empty', () => {
vm.actions[0].isLoading = true; expect(findActionButtons()).toBeNull();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .gl-spinner').getAttribute('style')).toBeFalsy();
done();
}); });
}); });
it('should render sidebar toggle button', () => { describe('slot', () => {
expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull(); it('should render header action buttons', () => {
vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
const buttons = findActionButtons();
expect(buttons).not.toBeNull();
expect(buttons.textContent).toEqual('Test Actions');
}); });
}); });
......
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