Commit f82c3f2d authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'move-pipeline-header-actions-to-graphql' into 'master'

Refactor pipeline header to use GraphQL mutations

See merge request gitlab-org/gitlab!47011
parents c982e3c3 0b45caac
<script> <script>
import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import ciHeader from '~/vue_shared/components/header_ci_component.vue'; import ciHeader from '~/vue_shared/components/header_ci_component.vue';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
const DELETE_MODAL_ID = 'pipeline-delete-modal'; const DELETE_MODAL_ID = 'pipeline-delete-modal';
const POLL_INTERVAL = 10000;
export default { export default {
name: 'PipelineHeaderSection', name: 'PipelineHeaderSection',
pipelineCancel: 'pipelineCancel',
pipelineRetry: 'pipelineRetry',
components: { components: {
ciHeader, ciHeader,
GlAlert, GlAlert,
...@@ -28,7 +33,7 @@ export default { ...@@ -28,7 +33,7 @@ export default {
[DEFAULT]: __('An unknown error occurred.'), [DEFAULT]: __('An unknown error occurred.'),
}, },
inject: { inject: {
// Receive `cancel`, `delete`, `fullProject` and `retry` // Receive `fullProject` and `pipelinesPath`
paths: { paths: {
default: {}, default: {},
}, },
...@@ -52,7 +57,7 @@ export default { ...@@ -52,7 +57,7 @@ export default {
error() { error() {
this.reportFailure(LOAD_FAILURE); this.reportFailure(LOAD_FAILURE);
}, },
pollInterval: 10000, pollInterval: POLL_INTERVAL,
watchLoading(isLoading) { watchLoading(isLoading) {
if (!isLoading) { if (!isLoading) {
// To ensure apollo has updated the cache, // To ensure apollo has updated the cache,
...@@ -122,31 +127,58 @@ export default { ...@@ -122,31 +127,58 @@ export default {
reportFailure(errorType) { reportFailure(errorType) {
this.failureType = errorType; this.failureType = errorType;
}, },
async postAction(path) { async postPipelineAction(name, mutation) {
try { try {
await axios.post(path); const {
this.$apollo.queries.pipeline.refetch(); data: {
[name]: { errors },
},
} = await this.$apollo.mutate({
mutation,
variables: { id: this.pipeline.id },
});
if (errors.length > 0) {
this.reportFailure(POST_FAILURE);
} else {
this.$apollo.queries.pipeline.refetch();
}
} catch { } catch {
this.reportFailure(POST_FAILURE); this.reportFailure(POST_FAILURE);
} }
}, },
async cancelPipeline() { cancelPipeline() {
this.isCanceling = true; this.isCanceling = true;
this.postAction(this.paths.cancel); this.postPipelineAction(this.$options.pipelineCancel, cancelPipelineMutation);
}, },
async retryPipeline() { retryPipeline() {
this.isRetrying = true; this.isRetrying = true;
this.postAction(this.paths.retry); this.postPipelineAction(this.$options.pipelineRetry, retryPipelineMutation);
}, },
async deletePipeline() { async deletePipeline() {
this.isDeleting = true; this.isDeleting = true;
this.$apollo.queries.pipeline.stopPolling(); this.$apollo.queries.pipeline.stopPolling();
try { try {
const { request } = await axios.delete(this.paths.delete); const {
redirectTo(setUrlFragment(request.responseURL, 'delete_success')); data: {
pipelineDestroy: { errors },
},
} = await this.$apollo.mutate({
mutation: deletePipelineMutation,
variables: {
id: this.pipeline.id,
},
});
if (errors.length > 0) {
this.reportFailure(DELETE_FAILURE);
this.isDeleting = false;
} else {
redirectTo(setUrlFragment(this.paths.pipelinesPath, 'delete_success'));
}
} catch { } catch {
this.$apollo.queries.pipeline.startPolling(); this.$apollo.queries.pipeline.startPolling(POLL_INTERVAL);
this.reportFailure(DELETE_FAILURE); this.reportFailure(DELETE_FAILURE);
this.isDeleting = false; this.isDeleting = false;
} }
......
mutation cancelPipeline($id: CiPipelineID!) {
pipelineCancel(input: { id: $id }) {
errors
}
}
mutation deletePipeline($id: CiPipelineID!) {
pipelineDestroy(input: { id: $id }) {
errors
}
}
mutation retryPipeline($id: CiPipelineID!) {
pipelineRetry(input: { id: $id }) {
errors
}
}
...@@ -16,7 +16,7 @@ export const createPipelineHeaderApp = elSelector => { ...@@ -16,7 +16,7 @@ export const createPipelineHeaderApp = elSelector => {
return; return;
} }
const { cancelPath, deletePath, fullPath, pipelineId, pipelineIid, retryPath } = el?.dataset; const { fullPath, pipelineId, pipelineIid, pipelinesPath } = el?.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
...@@ -26,10 +26,8 @@ export const createPipelineHeaderApp = elSelector => { ...@@ -26,10 +26,8 @@ export const createPipelineHeaderApp = elSelector => {
apolloProvider, apolloProvider,
provide: { provide: {
paths: { paths: {
cancel: cancelPath,
delete: deletePath,
fullProject: fullPath, fullProject: fullPath,
retry: retryPath, pipelinesPath,
}, },
pipelineId, pipelineId,
pipelineIid, pipelineIid,
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- add_page_specific_style 'page_bundles/ci_status' - add_page_specific_style 'page_bundles/ci_status'
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } } .js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-header-vue.pipeline-header-container{ data: {full_path: @project.full_path, retry_path: retry_project_pipeline_path(@pipeline.project, @pipeline), cancel_path: cancel_project_pipeline_path(@pipeline.project, @pipeline), delete_path: project_pipeline_path(@pipeline.project, @pipeline), pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id} } #js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
- if @pipeline.commit.present? - if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit = render "projects/pipelines/info", commit: @pipeline.commit
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlModal, GlLoadingIcon } from '@gitlab/ui'; import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { import {
mockCancelledPipelineHeader, mockCancelledPipelineHeader,
mockFailedPipelineHeader, mockFailedPipelineHeader,
mockRunningPipelineHeader, mockRunningPipelineHeader,
mockSuccessfulPipelineHeader, mockSuccessfulPipelineHeader,
} from './mock_data'; } from './mock_data';
import axios from '~/lib/utils/axios_utils';
import HeaderComponent from '~/pipelines/components/header_component.vue'; import HeaderComponent from '~/pipelines/components/header_component.vue';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
describe('Pipeline details header', () => { describe('Pipeline details header', () => {
let wrapper; let wrapper;
let glModalDirective; let glModalDirective;
let mockAxios;
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]'); const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
...@@ -25,9 +25,7 @@ describe('Pipeline details header', () => { ...@@ -25,9 +25,7 @@ describe('Pipeline details header', () => {
pipelineId: 14, pipelineId: 14,
pipelineIid: 1, pipelineIid: 1,
paths: { paths: {
retry: '/retry', pipelinesPath: '/namespace/my-project/-/pipelines',
cancel: '/cancel',
delete: '/delete',
fullProject: '/namespace/my-project', fullProject: '/namespace/my-project',
}, },
}; };
...@@ -43,6 +41,7 @@ describe('Pipeline details header', () => { ...@@ -43,6 +41,7 @@ describe('Pipeline details header', () => {
startPolling: jest.fn(), startPolling: jest.fn(),
}, },
}, },
mutate: jest.fn(),
}; };
return shallowMount(HeaderComponent, { return shallowMount(HeaderComponent, {
...@@ -65,16 +64,9 @@ describe('Pipeline details header', () => { ...@@ -65,16 +64,9 @@ describe('Pipeline details header', () => {
}); });
}; };
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet('*').replyOnce(200);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
mockAxios.restore();
}); });
describe('initial loading', () => { describe('initial loading', () => {
...@@ -111,13 +103,13 @@ describe('Pipeline details header', () => { ...@@ -111,13 +103,13 @@ describe('Pipeline details header', () => {
wrapper = createComponent(mockCancelledPipelineHeader); wrapper = createComponent(mockCancelledPipelineHeader);
}); });
it('should call axios with the right path when retry button is clicked', async () => { it('should call retryPipeline Mutation with pipeline id', () => {
jest.spyOn(axios, 'post');
findRetryButton().vm.$emit('click'); findRetryButton().vm.$emit('click');
await wrapper.vm.$nextTick(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: retryPipelineMutation,
expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.retry); variables: { id: mockCancelledPipelineHeader.id },
});
}); });
}); });
...@@ -126,13 +118,13 @@ describe('Pipeline details header', () => { ...@@ -126,13 +118,13 @@ describe('Pipeline details header', () => {
wrapper = createComponent(mockRunningPipelineHeader); wrapper = createComponent(mockRunningPipelineHeader);
}); });
it('should call axios with the right path when cancel button is clicked', async () => { it('should call cancelPipeline Mutation with pipeline id', () => {
jest.spyOn(axios, 'post');
findCancelButton().vm.$emit('click'); findCancelButton().vm.$emit('click');
await wrapper.vm.$nextTick(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: cancelPipelineMutation,
expect(axios.post).toHaveBeenCalledWith(defaultProvideOptions.paths.cancel); variables: { id: mockRunningPipelineHeader.id },
});
}); });
}); });
...@@ -141,24 +133,21 @@ describe('Pipeline details header', () => { ...@@ -141,24 +133,21 @@ describe('Pipeline details header', () => {
wrapper = createComponent(mockFailedPipelineHeader); wrapper = createComponent(mockFailedPipelineHeader);
}); });
it('displays delete modal when clicking on delete and does not call the delete action', async () => { it('displays delete modal when clicking on delete and does not call the delete action', () => {
jest.spyOn(axios, 'delete');
findDeleteButton().vm.$emit('click'); findDeleteButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID); expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID); expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
expect(axios.delete).not.toHaveBeenCalled(); expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
}); });
it('should call delete path when modal is submitted', async () => { it('should call deletePipeline Mutation with pipeline id when modal is submitted', () => {
jest.spyOn(axios, 'delete');
findDeleteModal().vm.$emit('ok'); findDeleteModal().vm.$emit('ok');
await wrapper.vm.$nextTick(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: deletePipelineMutation,
expect(axios.delete).toHaveBeenCalledWith(defaultProvideOptions.paths.delete); variables: { id: mockFailedPipelineHeader.id },
});
}); });
}); });
}); });
......
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