Commit cfba59ce authored by Phil Hughes's avatar Phil Hughes

Merge branch '45715-remove-modal-retry' into 'master'

Resolve "Remove modal box confirmation when retrying a pipeline"

Closes #45715

See merge request gitlab-org/gitlab-ce!18879
parents 27d0c429 feb9caab
<script>
/* eslint-disable no-alert */
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
components: {
loadingIcon,
icon,
},
props: {
endpoint: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
cssClass: {
type: String,
required: true,
},
pipelineId: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
buttonClass() {
return `btn ${this.cssClass}`;
},
},
created() {
// We're using eventHub to listen to the modal here instead of
// using props because it would would make the parent components
// much more complex to keep track of the loading state of each button
eventHub.$on('postAction', this.setLoading);
},
beforeDestroy() {
eventHub.$off('postAction', this.setLoading);
},
methods: {
onClick() {
eventHub.$emit('openConfirmationModal', {
pipelineId: this.pipelineId,
endpoint: this.endpoint,
type: this.type,
});
},
setLoading(endpoint) {
if (endpoint === this.endpoint) {
this.isLoading = true;
}
},
},
};
</script>
<template>
<button
v-tooltip
type="button"
@click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
data-container="body"
data-placement="top"
:disabled="isLoading">
<icon
:name="icon"
/>
<loading-icon v-if="isLoading" />
</button>
</template>
<script> <script>
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
*/ */
export default { export default {
components: { components: {
pipelinesTableRowComponent, PipelinesTableRowComponent,
DeprecatedModal, Modal,
}, },
props: { props: {
pipelines: { pipelines: {
...@@ -37,30 +37,18 @@ ...@@ -37,30 +37,18 @@
return { return {
pipelineId: '', pipelineId: '',
endpoint: '', endpoint: '',
type: '',
}; };
}, },
computed: { computed: {
modalTitle() { modalTitle() {
return this.type === 'stop' ? return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { pipelineId: `${this.pipelineId}`,
pipelineId: `'${this.pipelineId}'`, }, false);
}, false) :
sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false);
}, },
modalText() { modalText() {
return this.type === 'stop' ? return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { pipelineId: `<strong>#${this.pipelineId}</strong>`,
pipelineId: `<strong>#${this.pipelineId}</strong>`, }, false);
}, false) :
sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false);
},
primaryButtonLabel() {
return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
}, },
}, },
created() { created() {
...@@ -73,7 +61,6 @@ ...@@ -73,7 +61,6 @@
setModalData(data) { setModalData(data) {
this.pipelineId = data.pipelineId; this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint; this.endpoint = data.endpoint;
this.type = data.type;
}, },
onSubmit() { onSubmit() {
eventHub.$emit('postAction', this.endpoint); eventHub.$emit('postAction', this.endpoint);
...@@ -120,20 +107,16 @@ ...@@ -120,20 +107,16 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
<deprecated-modal
<modal
id="confirmation-modal" id="confirmation-modal"
:title="modalTitle" :header-title-text="modalTitle"
:text="modalText" footer-primary-button-variant="danger"
kind="danger" :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit" @submit="onSubmit"
> >
<template <span v-html="modalText"></span>
slot="body" </modal>
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</deprecated-modal>
</div> </div>
</template> </template>
<script> <script>
/* eslint-disable no-param-reassign */ import eventHub from '../event_hub';
import asyncButtonComponent from './async_button.vue'; import PipelinesActionsComponent from './pipelines_actions.vue';
import pipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; import PipelineStage from './stage.vue';
import pipelineStage from './stage.vue'; import PipelineUrl from './pipeline_url.vue';
import pipelineUrl from './pipeline_url.vue'; import PipelinesTimeago from './time_ago.vue';
import pipelinesTimeago from './time_ago.vue'; import CommitComponent from '../../vue_shared/components/commit.vue';
import commitComponent from '../../vue_shared/components/commit.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -16,14 +17,15 @@ ...@@ -16,14 +17,15 @@
*/ */
export default { export default {
components: { components: {
asyncButtonComponent, PipelinesActionsComponent,
pipelinesActionsComponent, PipelinesArtifactsComponent,
pipelinesArtifactsComponent, CommitComponent,
commitComponent, PipelineStage,
pipelineStage, PipelineUrl,
pipelineUrl, CiBadge,
ciBadge, PipelinesTimeago,
pipelinesTimeago, LoadingButton,
Icon,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -44,6 +46,12 @@ ...@@ -44,6 +46,12 @@
required: true, required: true,
}, },
}, },
data() {
return {
isRetrying: false,
isCancelling: false,
};
},
computed: { computed: {
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
...@@ -119,8 +127,10 @@ ...@@ -119,8 +127,10 @@
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') { if (prop === 'path') {
// eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
// eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
return accumulator; return accumulator;
...@@ -216,6 +226,21 @@ ...@@ -216,6 +226,21 @@
return this.viewType === 'child'; return this.viewType === 'child';
}, },
}, },
methods: {
handleCancelClick() {
this.isCancelling = true;
eventHub.$emit('openConfirmationModal', {
pipelineId: this.pipeline.id,
endpoint: this.pipeline.cancel_path,
});
},
handleRetryClick() {
this.isRetrying = true;
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
},
},
}; };
</script> </script>
<template> <template>
...@@ -287,7 +312,8 @@ ...@@ -287,7 +312,8 @@
<div <div
v-if="displayPipelineActions" v-if="displayPipelineActions"
class="table-section section-20 table-button-footer pipeline-actions"> class="table-section section-20 table-button-footer pipeline-actions"
>
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
...@@ -300,29 +326,27 @@ ...@@ -300,29 +326,27 @@
:artifacts="pipeline.details.artifacts" :artifacts="pipeline.details.artifacts"
/> />
<async-button-component <loading-button
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:endpoint="pipeline.retry_path" @click="handleRetryClick"
css-class="js-pipelines-retry-button btn-default btn-retry" container-class="js-pipelines-retry-button btn btn-default btn-retry"
title="Retry" :loading="isRetrying"
icon="repeat" :disabled="isRetrying"
:pipeline-id="pipeline.id" >
data-toggle="modal" <icon name="repeat" />
data-target="#confirmation-modal" </loading-button>
type="retry"
/>
<async-button-component <loading-button
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path" @click="handleCancelClick"
css-class="js-pipelines-cancel-button btn-remove"
title="Stop"
icon="close"
:pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#confirmation-modal" data-target="#confirmation-modal"
type="stop" container-class="js-pipelines-cancel-button btn btn-remove"
/> :loading="isCancelling"
:disabled="isCancelling"
>
<icon name="close" />
</loading-button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -53,10 +53,12 @@ export default { ...@@ -53,10 +53,12 @@ export default {
}); });
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable); eventHub.$on('clickedDropdown', this.updateTable);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('postAction', this.postAction); eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable); eventHub.$off('clickedDropdown', this.updateTable);
}, },
destroyed() { destroyed() {
......
...@@ -70,12 +70,14 @@ ...@@ -70,12 +70,14 @@
/> />
</transition> </transition>
<transition name="fade"> <transition name="fade">
<span <slot>
v-if="label" <span
class="js-loading-button-label" v-if="label"
> class="js-loading-button-label"
{{ label }} >
</span> {{ label }}
</span>
</slot>
</transition> </transition>
</button> </button>
</template> </template>
---
title: Remove modalbox confirmation when retrying a pipeline
merge_request: 18879
author:
type: changed
...@@ -125,7 +125,7 @@ describe 'Pipelines', :js do ...@@ -125,7 +125,7 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
find('.js-pipelines-cancel-button').click find('.js-pipelines-cancel-button').click
find('.js-primary-button').click find('.js-modal-primary-action').click
wait_for_requests wait_for_requests
end end
...@@ -156,7 +156,6 @@ describe 'Pipelines', :js do ...@@ -156,7 +156,6 @@ describe 'Pipelines', :js do
context 'when retrying' do context 'when retrying' do
before do before do
find('.js-pipelines-retry-button').click find('.js-pipelines-retry-button').click
find('.js-primary-button').click
wait_for_requests wait_for_requests
end end
...@@ -256,7 +255,7 @@ describe 'Pipelines', :js do ...@@ -256,7 +255,7 @@ describe 'Pipelines', :js do
context 'when canceling' do context 'when canceling' do
before do before do
find('.js-pipelines-cancel-button').click find('.js-pipelines-cancel-button').click
find('.js-primary-button').click find('.js-modal-primary-action').click
end end
it 'indicates that pipeline was canceled' do it 'indicates that pipeline was canceled' do
......
import Vue from 'vue';
import asyncButtonComp from '~/pipelines/components/async_button.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipelines Async Button', () => {
let component;
let AsyncButtonComponent;
beforeEach(() => {
AsyncButtonComponent = Vue.extend(asyncButtonComp);
component = new AsyncButtonComponent({
propsData: {
endpoint: '/foo',
title: 'Foo',
icon: 'repeat',
cssClass: 'bar',
pipelineId: 123,
type: 'explode',
},
}).$mount();
});
it('should render a button', () => {
expect(component.$el.tagName).toEqual('BUTTON');
});
it('should render svg icon', () => {
expect(component.$el.querySelector('svg')).not.toBeNull();
});
it('should render the provided title', () => {
expect(component.$el.getAttribute('data-original-title')).toContain('Foo');
expect(component.$el.getAttribute('aria-label')).toContain('Foo');
});
it('should render the provided cssClass', () => {
expect(component.$el.getAttribute('class')).toContain('bar');
});
describe('With confirm dialog', () => {
it('should call the service when confimation is positive', () => {
eventHub.$on('openConfirmationModal', (data) => {
expect(data.pipelineId).toEqual(123);
expect(data.type).toEqual('explode');
});
component = new AsyncButtonComponent({
propsData: {
endpoint: '/foo',
title: 'Foo',
icon: 'fa fa-foo',
cssClass: 'bar',
pipelineId: 123,
type: 'explode',
},
}).$mount();
component.$el.click();
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => { describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
...@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => { ...@@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => { describe('actions column', () => {
beforeEach(() => { beforeEach(() => {
component = buildComponent(pipeline); const withActions = Object.assign({}, pipeline);
withActions.flags.cancelable = true;
withActions.flags.retryable = true;
withActions.cancel_path = '/cancel';
withActions.retry_path = '/retry';
component = buildComponent(withActions);
}); });
it('should render the provided actions', () => { it('should render the provided actions', () => {
expect( expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
).toEqual(pipeline.details.manual_actions.length); });
it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
eventHub.$on('retryPipeline', (endpoint) => {
expect(endpoint).toEqual('/retry');
});
component.$el.querySelector('.js-pipelines-retry-button').click();
expect(component.isRetrying).toEqual(true);
});
it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
eventHub.$on('openConfirmationModal', (data) => {
expect(data.endpoint).toEqual('/cancel');
expect(data.pipelineId).toEqual(pipeline.id);
});
component.$el.querySelector('.js-pipelines-cancel-button').click();
expect(component.isCancelling).toEqual(true);
}); });
}); });
}); });
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