Commit f75fa8a4 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Miguel Rincon

Group-level "DevOps Adoption" - Sync graphql variables

parent f9014f33
......@@ -22,7 +22,7 @@ import {
import bulkFindOrCreateDevopsAdoptionSegmentsMutation from '../graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql';
import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.query.graphql';
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import { addSegmentsToCache } from '../utils/cache_updates';
import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates';
import { shouldPollTableData } from '../utils/helpers';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
......@@ -74,7 +74,7 @@ export default {
pageInfo: null,
},
pollingTableData: null,
variables: this.isGroup
segmentsQueryVariables: this.isGroup
? {
parentNamespaceId: this.groupGid,
directDescendantsOnly: false,
......@@ -86,7 +86,7 @@ export default {
devopsAdoptionSegments: {
query: devopsAdoptionSegmentsQuery,
variables() {
return this.variables;
return this.segmentsQueryVariables;
},
result({ data }) {
if (this.isGroup) {
......@@ -166,7 +166,7 @@ export default {
if (errors.length) {
this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.addSegment, errors);
} else {
addSegmentsToCache(store, segments, this.variables);
this.addSegmentsToCache(segments);
}
},
})
......@@ -232,6 +232,16 @@ export default {
clearSelectedSegment() {
this.selectedSegment = null;
},
addSegmentsToCache(segments) {
const { cache } = this.$apollo.getClient();
addSegmentsToCache(cache, segments, this.segmentsQueryVariables);
},
deleteSegmentsFromCache(ids) {
const { cache } = this.$apollo.getClient();
deleteSegmentsFromCache(cache, ids, this.segmentsQueryVariables);
},
},
};
</script>
......@@ -250,6 +260,8 @@ export default {
:key="modalKey"
:groups="groups.nodes"
:enabled-groups="devopsAdoptionSegments.nodes"
@segmentsAdded="addSegmentsToCache"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
<div v-if="hasSegmentsData" class="gl-mt-3">
......@@ -279,6 +291,7 @@ export default {
:segments="devopsAdoptionSegments.nodes"
:selected-segment="selectedSegment"
@set-selected-segment="setSelectedSegment"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
</div>
......
......@@ -3,7 +3,6 @@ import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID } from '../constants';
import deleteDevopsAdoptionSegmentMutation from '../graphql/mutations/delete_devops_adoption_segment.mutation.graphql';
import { deleteSegmentsFromCache } from '../utils/cache_updates';
export default {
name: 'DevopsAdoptionDeleteModal',
......@@ -65,8 +64,8 @@ export default {
variables: {
id: [id],
},
update(store) {
deleteSegmentsFromCache(store, [id]);
update: () => {
this.$emit('segmentsRemoved', [id]);
},
});
......
......@@ -10,7 +10,6 @@ import {
} from '../constants';
import bulkFindOrCreateDevopsAdoptionSegmentsMutation from '../graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql';
import deleteDevopsAdoptionSegmentMutation from '../graphql/mutations/delete_devops_adoption_segment.mutation.graphql';
import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates';
export default {
name: 'DevopsAdoptionSegmentModal',
......@@ -26,6 +25,9 @@ export default {
isGroup: {
default: false,
},
groupGid: {
default: null,
},
},
props: {
groups: {
......@@ -139,7 +141,7 @@ export default {
bulkFindOrCreateDevopsAdoptionSegments: { segments, errors: requestErrors },
} = data;
if (!requestErrors.length) addSegmentsToCache(store, segments);
if (!requestErrors.length) this.$emit('segmentsAdded', segments);
},
});
......@@ -157,7 +159,11 @@ export default {
async deleteMissingGroups() {
try {
const removedGroupGids = this.enabledGroups
.filter((group) => !this.checkboxValues.includes(getIdFromGraphQLId(group.namespace.id)))
.filter(
(group) =>
!this.checkboxValues.includes(getIdFromGraphQLId(group.namespace.id)) &&
group.namespace.id !== this.groupGid,
)
.map((group) => group.id);
if (removedGroupGids.length) {
......@@ -177,7 +183,7 @@ export default {
deleteDevopsAdoptionSegment: { errors: requestErrors },
} = data;
if (!requestErrors.length) deleteSegmentsFromCache(store, removedGroupGids);
if (!requestErrors.length) this.$emit('segmentsRemoved', removedGroupGids);
},
});
......@@ -199,7 +205,6 @@ export default {
this.$refs.modal.hide();
},
resetForm() {
this.checkboxValues = [];
this.filter = '';
this.$emit('trackModalOpenState', false);
},
......
......@@ -253,6 +253,7 @@ export default {
<devops-adoption-delete-modal
v-if="selectedSegment"
:segment="selectedSegment"
@segmentsRemoved="$emit('segmentsRemoved', $event)"
@trackModalOpenState="$emit('trackModalOpenState', $event)"
/>
</div>
......
query($parentNamespaceId: NamespaceID, $directDescendantsOnly: Boolean) {
query devopsAdoptionSegments($parentNamespaceId: NamespaceID, $directDescendantsOnly: Boolean) {
devopsAdoptionSegments(
parentNamespaceId: $parentNamespaceId
directDescendantsOnly: $directDescendantsOnly
......
......@@ -21,9 +21,10 @@ export const addSegmentsToCache = (store, segments, variables) => {
});
};
export const deleteSegmentsFromCache = (store, segmentIds) => {
export const deleteSegmentsFromCache = (store, segmentIds, variables) => {
const sourceData = store.readQuery({
query: devopsAdoptionSegmentsQuery,
variables,
});
const updatedData = produce(sourceData, (draftData) => {
......@@ -34,6 +35,7 @@ export const deleteSegmentsFromCache = (store, segmentIds) => {
store.writeQuery({
query: devopsAdoptionSegmentsQuery,
variables,
data: updatedData,
});
};
......@@ -515,7 +515,18 @@ describe('DevopsAdoptionApp', () => {
namespaceIds: [groupGid],
}),
);
});
it('calls addSegmentsToCache with the correct variables', () => {
expect(addSegmentsToCache).toHaveBeenCalledTimes(1);
expect(addSegmentsToCache).toHaveBeenCalledWith(
expect.anything(),
[devopsAdoptionSegmentsData.nodes[0]],
{
parentNamespaceId: groupGid,
directDescendantsOnly: false,
},
);
});
describe('error handling', () => {
......
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import DevopsAdoptionDeleteModal from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_delete_modal.vue';
import { DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID } from 'ee/analytics/devops_report/devops_adoption/constants';
import deleteDevopsAdoptionSegmentMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/delete_devops_adoption_segment.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
genericDeleteErrorMessage,
......@@ -11,6 +14,9 @@ import {
devopsAdoptionSegmentsData,
} from '../mock_data';
const localVue = createLocalVue();
Vue.use(VueApollo);
const mockEvent = { preventDefault: jest.fn() };
const mutate = jest.fn().mockResolvedValue({
data: {
......@@ -32,21 +38,18 @@ const mutateWithErrors = jest.fn().mockRejectedValue(genericDeleteErrorMessage);
describe('DevopsAdoptionDeleteModal', () => {
let wrapper;
const createComponent = ({ mutationMock = mutate } = {}) => {
const $apollo = {
mutate: mutationMock,
};
const createComponent = ({ deleteSegmentsSpy = mutate, props = {} } = {}) => {
const mockApollo = createMockApollo([[deleteDevopsAdoptionSegmentMutation, deleteSegmentsSpy]]);
wrapper = shallowMount(DevopsAdoptionDeleteModal, {
localVue,
apolloProvider: mockApollo,
propsData: {
segment: devopsAdoptionSegmentsData.nodes[0],
...props,
},
stubs: {
GlSprintf,
ApolloMutation,
},
mocks: {
$apollo,
},
});
};
......@@ -99,7 +102,7 @@ describe('DevopsAdoptionDeleteModal', () => {
describe('submitting the form', () => {
describe('while waiting for the mutation', () => {
beforeEach(() => createComponent({ mutationMock: mutateLoading }));
beforeEach(() => createComponent({ deleteSegmentsSpy: mutateLoading }));
it('disables the cancel button', async () => {
expect(cancelButtonDisabledState()).toBe(false);
......@@ -132,13 +135,15 @@ describe('DevopsAdoptionDeleteModal', () => {
});
it('submits the correct request variables', () => {
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
expect(mutate).toHaveBeenCalledWith({
id: [devopsAdoptionSegmentsData.nodes[0].id],
},
}),
);
});
});
it('emits segmentsRemoved with the correct variables', () => {
const [params] = wrapper.emitted().segmentsRemoved[0];
expect(params).toStrictEqual([devopsAdoptionSegmentsData.nodes[0].id]);
});
it('closes the modal after a successful mutation', () => {
......@@ -154,7 +159,7 @@ describe('DevopsAdoptionDeleteModal', () => {
`(
'displays a $errorType error if the mutation has a $errorLocation error',
async ({ mutationSpy, message }) => {
createComponent({ mutationMock: mutationSpy });
createComponent({ deleteSegmentsSpy: mutationSpy });
findModal().vm.$emit('primary', mockEvent);
......@@ -171,13 +176,15 @@ describe('DevopsAdoptionDeleteModal', () => {
it('calls sentry on top level error', async () => {
jest.spyOn(Sentry, 'captureException');
createComponent({ mutationMock: mutateWithErrors });
createComponent({ deleteSegmentsSpy: mutateWithErrors });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
expect(Sentry.captureException.mock.calls[0][0]).toBe(genericDeleteErrorMessage);
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(
genericDeleteErrorMessage,
);
});
});
});
......
import { GlModal, GlFormInput, GlSprintf, GlAlert, GlIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { ApolloMutation } from 'vue-apollo';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import DevopsAdoptionSegmentModal from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_segment_modal.vue';
import { DEVOPS_ADOPTION_SEGMENT_MODAL_ID } from 'ee/analytics/devops_report/devops_adoption/constants';
import bulkFindOrCreateDevopsAdoptionSegmentsMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/bulk_find_or_create_devops_adoption_segments.mutation.graphql';
import deleteDevopsAdoptionSegmentMutation from 'ee/analytics/devops_report/devops_adoption/graphql/mutations/delete_devops_adoption_segment.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
groupNodes,
......@@ -16,13 +19,18 @@ import {
devopsAdoptionSegmentsData,
} from '../mock_data';
const localVue = createLocalVue();
Vue.use(VueApollo);
const mockEvent = { preventDefault: jest.fn() };
const mutate = jest.fn().mockResolvedValue({
data: {
bulkFindOrCreateDevopsAdoptionSegments: {
segments: [devopsAdoptionSegmentsData.nodes[0]],
errors: [],
},
deleteDevopsAdoptionSegment: {
segments: [devopsAdoptionSegmentsData.nodes[0]],
errors: [],
},
},
......@@ -31,9 +39,11 @@ const mutateWithDataErrors = jest.fn().mockResolvedValue({
data: {
bulkFindOrCreateDevopsAdoptionSegments: {
errors: [dataErrorMessage],
segments: [],
},
deleteDevopsAdoptionSegment: {
errors: [],
errors: [dataErrorMessage],
segments: [],
},
},
});
......@@ -43,12 +53,20 @@ const mutateWithErrors = jest.fn().mockRejectedValue(genericErrorMessage);
describe('DevopsAdoptionSegmentModal', () => {
let wrapper;
const createComponent = ({ mutationMock = mutate, props = {}, provide = {} } = {}) => {
const $apollo = {
mutate: mutationMock,
};
const createComponent = ({
deleteSegmentsSpy = mutate,
addSegmentsSpy = mutate,
props = {},
provide = {},
} = {}) => {
const mockApollo = createMockApollo([
[deleteDevopsAdoptionSegmentMutation, deleteSegmentsSpy],
[bulkFindOrCreateDevopsAdoptionSegmentsMutation, addSegmentsSpy],
]);
wrapper = shallowMount(DevopsAdoptionSegmentModal, {
localVue,
apolloProvider: mockApollo,
propsData: {
groups: groupNodes,
...props,
......@@ -56,12 +74,10 @@ describe('DevopsAdoptionSegmentModal', () => {
provide,
stubs: {
GlSprintf,
ApolloMutation,
},
mocks: {
$apollo,
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
};
const findModal = () => wrapper.find(GlModal);
......@@ -274,14 +290,11 @@ describe('DevopsAdoptionSegmentModal', () => {
`(
'$action groups',
({ enabledGroups, newGroups, expectedAddGroupGids, expectedDeleteIds }) => {
describe('successful submission', () => {
beforeEach(async () => {
createComponent({ props: { enabledGroups } });
wrapper.setData(newGroups);
wrapper.vm.$refs.modal.hide = jest.fn();
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
......@@ -289,21 +302,25 @@ describe('DevopsAdoptionSegmentModal', () => {
if (expectedAddGroupGids.length) {
it('submits the correct add request variables', () => {
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: { namespaceIds: expectedAddGroupGids },
}),
);
expect(mutate).toHaveBeenCalledWith({ namespaceIds: expectedAddGroupGids });
});
it('emits segmentsAdded with the correct variables', () => {
const [params] = wrapper.emitted().segmentsAdded[0];
expect(params).toStrictEqual([devopsAdoptionSegmentsData.nodes[0]]);
});
}
if (expectedDeleteIds.length) {
it('submits the correct delete request variables', () => {
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: { id: expectedDeleteIds },
}),
);
expect(mutate).toHaveBeenCalledWith({ id: expectedDeleteIds });
});
it('emits segmentsRemoved with the correct variables', () => {
const [params] = wrapper.emitted().segmentsRemoved[0];
expect(params).toStrictEqual(firstGroupId);
});
}
......@@ -311,13 +328,11 @@ describe('DevopsAdoptionSegmentModal', () => {
expect(wrapper.vm.$refs.modal.hide).toHaveBeenCalled();
});
it('resets the form fields', () => {
it('resets the filter', () => {
findModal().vm.$emit('hidden');
expect(wrapper.vm.checkboxValues).toEqual([]);
expect(wrapper.vm.filter).toBe('');
});
});
},
);
......@@ -329,7 +344,7 @@ describe('DevopsAdoptionSegmentModal', () => {
`(
'displays a $errorType error if the mutation has a $errorLocation error',
async ({ mutationSpy, message }) => {
createComponent({ mutationMock: mutationSpy });
createComponent({ addSegmentsSpy: mutationSpy, deleteSegmentsSpy: mutationSpy });
wrapper.setData(enableFirstGroup);
......@@ -348,7 +363,10 @@ describe('DevopsAdoptionSegmentModal', () => {
it('calls sentry on top level error', async () => {
const captureException = jest.spyOn(Sentry, 'captureException');
createComponent({ mutationMock: mutateWithErrors });
createComponent({
addSegmentsSpy: mutateWithErrors,
deleteSegmentsSpy: mutateWithErrors,
});
wrapper.setData(enableFirstGroup);
......@@ -356,7 +374,7 @@ describe('DevopsAdoptionSegmentModal', () => {
await waitForPromises();
expect(captureException).toHaveBeenCalledWith(genericErrorMessage);
expect(captureException.mock.calls[0][0].networkError).toBe(genericErrorMessage);
});
});
});
......
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