Commit 52716b39 authored by Fabio Huser's avatar Fabio Huser

refactor(pipelines): rework pipeline deletion to use AJAX

parent 93d3a4d2
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -9,6 +9,7 @@ export default { ...@@ -9,6 +9,7 @@ export default {
components: { components: {
ciHeader, ciHeader,
GlLoadingIcon, GlLoadingIcon,
GlModal,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -49,6 +50,13 @@ export default { ...@@ -49,6 +50,13 @@ export default {
eventHub.$emit('headerPostAction', action); eventHub.$emit('headerPostAction', action);
}, },
deletePipeline() {
const index = this.actions.findIndex(action => action.modal === 'pipeline-delete-modal');
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerDeleteAction', this.actions[index]);
},
getActions() { getActions() {
const actions = []; const actions = [];
...@@ -77,10 +85,9 @@ export default { ...@@ -77,10 +85,9 @@ export default {
actions.push({ actions.push({
label: __('Delete'), label: __('Delete'),
path: this.pipeline.delete_path, path: this.pipeline.delete_path,
method: 'delete', modal: 'pipeline-delete-modal',
confirm: __('Are you sure you want to delete this pipeline?'),
cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted', cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted',
type: 'ujs-link', type: 'modal-button',
isLoading: false, isLoading: false,
}); });
} }
...@@ -102,6 +109,23 @@ export default { ...@@ -102,6 +109,23 @@ export default {
item-name="Pipeline" item-name="Pipeline"
@actionClicked="postAction" @actionClicked="postAction"
/> />
<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" />
<gl-modal
modal-id="pipeline-delete-modal"
:title="__('Delete pipeline')"
:ok-title="__('Delete pipeline')"
ok-variant="danger"
@ok="deletePipeline()"
>
<p>
{{
__(
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts and triggers. This action cannot be undone.',
)
}}
</p>
</gl-modal>
</div> </div>
</template> </template>
import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlToast } from '@gitlab/ui';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { __ } from '../../locale'; import { __ } from '../../locale';
import createFlash from '../../flash'; import createFlash from '../../flash';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
...@@ -9,6 +12,8 @@ import PipelinesTableComponent from '../components/pipelines_table.vue'; ...@@ -9,6 +12,8 @@ import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { CANCEL_REQUEST } from '../constants'; import { CANCEL_REQUEST } from '../constants';
Vue.use(GlToast);
export default { export default {
components: { components: {
PipelinesTableComponent, PipelinesTableComponent,
...@@ -57,6 +62,11 @@ export default { ...@@ -57,6 +62,11 @@ export default {
} }
}); });
if (doesHashExistInUrl('delete_success')) {
this.$toast.show(__('The pipeline has been deleted'));
historyPushState(buildUrlWithCurrentLocation());
}
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction); eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable); eventHub.$on('clickedDropdown', this.updateTable);
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import Flash from '~/flash'; import Flash from '~/flash';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator'; import PipelinesMediator from './pipeline_details_mediator';
...@@ -62,9 +63,11 @@ export default () => { ...@@ -62,9 +63,11 @@ export default () => {
}, },
created() { created() {
eventHub.$on('headerPostAction', this.postAction); eventHub.$on('headerPostAction', this.postAction);
eventHub.$on('headerDeleteAction', this.deleteAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction); eventHub.$off('headerPostAction', this.postAction);
eventHub.$off('headerDeleteAction', this.deleteAction);
}, },
methods: { methods: {
postAction(action) { postAction(action) {
...@@ -73,6 +76,15 @@ export default () => { ...@@ -73,6 +76,15 @@ export default () => {
.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) {
this.mediator.stopPipelinePoll();
this.mediator.service
.deleteAction(action.path)
.then(response =>
redirectTo(setUrlFragment(response.request.responseURL, 'delete_success')),
)
.catch(() => Flash(__('An error occurred while deleting the pipeline.')));
},
}, },
render(createElement) { render(createElement) {
return createElement('pipeline-header', { return createElement('pipeline-header', {
......
...@@ -64,6 +64,10 @@ export default class pipelinesMediator { ...@@ -64,6 +64,10 @@ export default class pipelinesMediator {
); );
} }
stopPipelinePoll() {
this.poll.stop();
}
/** /**
* Backend expects paramets in the following format: `expanded[]=id&expanded[]=id` * Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
*/ */
......
...@@ -9,6 +9,11 @@ export default class PipelineService { ...@@ -9,6 +9,11 @@ export default class PipelineService {
return axios.get(this.pipeline, { params }); return axios.get(this.pipeline, { params });
} }
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(`${endpoint}.json`);
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
postAction(endpoint) { postAction(endpoint) {
return axios.post(`${endpoint}.json`); return axios.post(`${endpoint}.json`);
......
<script> <script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlButton, GlModalDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; 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';
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
}, },
props: { props: {
status: { status: {
...@@ -117,36 +118,26 @@ export default { ...@@ -117,36 +118,26 @@ export default {
<section v-if="actions.length" class="header-action-buttons"> <section v-if="actions.length" class="header-action-buttons">
<template v-for="(action, i) in actions"> <template v-for="(action, i) in actions">
<gl-link <loading-button
v-if="action.type === 'link'" v-if="action.type === 'button'"
:key="i"
:href="action.path"
:class="action.cssClass"
>
{{ action.label }}
</gl-link>
<gl-link
v-else-if="action.type === 'ujs-link'"
:key="i" :key="i"
:href="action.path" :loading="action.isLoading"
:disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
:data-method="action.method" container-class="d-inline"
:data-confirm="action.confirm" :label="action.label"
rel="nofollow" @click="onClickAction(action)"
> />
{{ action.label }}
</gl-link>
<loading-button <loading-button
v-else-if="action.type === 'button'" v-else-if="action.type === 'modal-button'"
:key="i" :key="i"
v-gl-modal="action.modal"
:loading="action.isLoading" :loading="action.isLoading"
:disabled="action.isLoading" :disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
container-class="d-inline" container-class="d-inline"
:label="action.label" :label="action.label"
@click="onClickAction(action)"
/> />
</template> </template>
</section> </section>
......
...@@ -83,9 +83,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -83,9 +83,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def destroy def destroy
::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline) ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
redirect_to project_pipelines_path(project), redirect_to project_pipelines_path(project), status: :see_other
status: :found,
notice: _("Pipeline has been successfully deleted!")
end end
def builds def builds
......
...@@ -1631,6 +1631,9 @@ msgstr "" ...@@ -1631,6 +1631,9 @@ msgstr ""
msgid "An error occurred while deleting the comment" msgid "An error occurred while deleting the comment"
msgstr "" msgstr ""
msgid "An error occurred while deleting the pipeline."
msgstr ""
msgid "An error occurred while detecting host keys" msgid "An error occurred while detecting host keys"
msgstr "" msgstr ""
...@@ -2092,7 +2095,7 @@ msgstr "" ...@@ -2092,7 +2095,7 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?" msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "" msgstr ""
msgid "Are you sure you want to delete this pipeline?" msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts and triggers. This action cannot be undone."
msgstr "" msgstr ""
msgid "Are you sure you want to erase this build?" msgid "Are you sure you want to erase this build?"
...@@ -5735,6 +5738,9 @@ msgstr "" ...@@ -5735,6 +5738,9 @@ msgstr ""
msgid "Delete list" msgid "Delete list"
msgstr "" msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete snippet" msgid "Delete snippet"
msgstr "" msgstr ""
...@@ -12870,9 +12876,6 @@ msgstr "" ...@@ -12870,9 +12876,6 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "Pipeline has been successfully deleted!"
msgstr ""
msgid "Pipeline minutes quota" msgid "Pipeline minutes quota"
msgstr "" msgstr ""
...@@ -18083,6 +18086,9 @@ msgstr "" ...@@ -18083,6 +18086,9 @@ msgstr ""
msgid "The phase of the development lifecycle." msgid "The phase of the development lifecycle."
msgstr "" msgstr ""
msgid "The pipeline has been deleted"
msgstr ""
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user." msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
msgstr "" msgstr ""
......
...@@ -754,7 +754,7 @@ describe Projects::PipelinesController do ...@@ -754,7 +754,7 @@ describe Projects::PipelinesController do
it 'deletes pipeline and redirects' do it 'deletes pipeline and redirects' do
delete_pipeline delete_pipeline
expect(response).to have_gitlab_http_status(302) expect(response).to have_gitlab_http_status(303)
expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
......
...@@ -64,11 +64,12 @@ describe('Pipeline details header', () => { ...@@ -64,11 +64,12 @@ describe('Pipeline details header', () => {
vm.$el.querySelector('.js-retry-button').click(); vm.$el.querySelector('.js-retry-button').click();
}); });
it('should contain delete button with proper path and method', () => { it('should fire modal event when delete button action is clicked', () => {
const link = vm.$el.querySelector('.js-btn-delete-pipeline'); vm.$root.$on('bv::modal::show', action => {
expect(action.componentId).toEqual('pipeline-delete-modal');
});
expect(link.getAttribute('href')).toContain('path'); vm.$el.querySelector('.js-btn-delete-pipeline').click();
expect(link.getAttribute('data-method')).toContain('delete');
}); });
}); });
}); });
...@@ -35,20 +35,12 @@ describe('Header CI Component', () => { ...@@ -35,20 +35,12 @@ describe('Header CI Component', () => {
cssClass: 'btn', cssClass: 'btn',
isLoading: false, isLoading: false,
}, },
{
label: 'Go',
path: 'path',
type: 'link',
cssClass: 'link',
isLoading: false,
},
{ {
label: 'Delete', label: 'Delete',
path: 'path', path: 'path',
type: 'ujs-link', type: 'modal-button',
cssClass: 'ujs-link', cssClass: 'btn-modal',
method: 'delete', isLoading: false,
confirm: 'Are you sure?',
}, },
], ],
hasSidebarButton: true, hasSidebarButton: true,
...@@ -86,19 +78,12 @@ describe('Header CI Component', () => { ...@@ -86,19 +78,12 @@ describe('Header CI Component', () => {
it('should render provided actions', () => { it('should render provided actions', () => {
const btn = vm.$el.querySelector('.btn'); const btn = vm.$el.querySelector('.btn');
const link = vm.$el.querySelector('.link'); const btnModal = vm.$el.querySelector('.btn-modal');
const ujsLink = vm.$el.querySelector('.ujs-link');
expect(btn.tagName).toEqual('BUTTON'); expect(btn.tagName).toEqual('BUTTON');
expect(btn.textContent.trim()).toEqual(props.actions[0].label); expect(btn.textContent.trim()).toEqual(props.actions[0].label);
expect(link.tagName).toEqual('A'); expect(btnModal.tagName).toEqual('BUTTON');
expect(link.textContent.trim()).toEqual(props.actions[1].label); expect(btnModal.textContent.trim()).toEqual(props.actions[1].label);
expect(link.getAttribute('href')).toEqual(props.actions[0].path);
expect(ujsLink.tagName).toEqual('A');
expect(ujsLink.textContent.trim()).toEqual(props.actions[2].label);
expect(ujsLink.getAttribute('href')).toEqual(props.actions[2].path);
expect(ujsLink.getAttribute('data-method')).toEqual(props.actions[2].method);
expect(ujsLink.getAttribute('data-confirm')).toEqual(props.actions[2].confirm);
}); });
it('should show loading icon', done => { it('should show loading icon', done => {
......
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