Commit a3645c1c authored by David O'Regan's avatar David O'Regan

Merge branch '268361-delete-escalation-policy' into 'master'

Delete escalation policy

See merge request gitlab-org/gitlab!63774
parents d88e5224 e450fa08
<script>
import { GlSprintf, GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { updateStoreAfterEscalationPolicyDelete } from '../graphql/cache_updates';
import destroyEscalationPolicyMutation from '../graphql/mutations/destroy_escalatiion_policy.mutation.graphql';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
export const i18n = {
deleteEscalationPolicy: s__('EscalationPolicies|Delete escalation policy'),
deleteEscalationPolicyMessage: s__(
'EscalationPolicies|Are you sure you want to delete the "%{escalationPolicy}" escalation policy? This action cannot be undone.',
),
};
export default {
i18n,
components: {
GlSprintf,
GlModal,
GlAlert,
},
inject: ['projectPath'],
props: {
escalationPolicy: {
type: Object,
required: true,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
error: null,
};
},
computed: {
primaryProps() {
return {
text: this.$options.i18n.deleteEscalationPolicy,
attributes: [{ category: 'primary' }, { variant: 'danger' }, { loading: this.loading }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
},
methods: {
deleteEscalationPolicy() {
const {
escalationPolicy: { id },
projectPath,
} = this;
this.loading = true;
this.$apollo
.mutate({
mutation: destroyEscalationPolicyMutation,
variables: {
input: {
id,
},
},
update(store, { data }) {
updateStoreAfterEscalationPolicyDelete(store, getEscalationPoliciesQuery, data, {
projectPath,
});
},
})
.then(({ data: { escalationPolicyDestroy } = {} } = {}) => {
const error = escalationPolicyDestroy.errors[0];
if (error) {
throw error;
}
this.$refs.deleteEscalationPolicyModal.hide();
})
.catch((error) => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
hideErrorAlert() {
this.error = null;
},
},
};
</script>
<template>
<gl-modal
ref="deleteEscalationPolicyModal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.deleteEscalationPolicy"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary.prevent="deleteEscalationPolicy"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-sprintf :message="$options.i18n.deleteEscalationPolicyMessage">
<template #escalationPolicy>{{ escalationPolicy.name }}</template>
</gl-sprintf>
</gl-modal>
</template>
...@@ -10,7 +10,13 @@ import { ...@@ -10,7 +10,13 @@ import {
GlCollapse, GlCollapse,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { ACTIONS, ALERT_STATUSES, DEFAULT_ACTION } from '../constants'; import {
ACTIONS,
ALERT_STATUSES,
DEFAULT_ACTION,
deleteEscalationPolicyModalId,
} from '../constants';
import DeleteEscalationPolicyModal from './delete_escalation_policy_modal.vue';
export const i18n = { export const i18n = {
editPolicy: s__('EscalationPolicies|Edit escalation policy'), editPolicy: s__('EscalationPolicies|Edit escalation policy'),
...@@ -38,6 +44,7 @@ export default { ...@@ -38,6 +44,7 @@ export default {
GlSprintf, GlSprintf,
GlIcon, GlIcon,
GlCollapse, GlCollapse,
DeleteEscalationPolicyModal,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -68,11 +75,15 @@ export default { ...@@ -68,11 +75,15 @@ export default {
policyVisibleAngleIconLabel() { policyVisibleAngleIconLabel() {
return this.isPolicyVisible ? __('Collapse') : __('Expand'); return this.isPolicyVisible ? __('Collapse') : __('Expand');
}, },
deletePolicyModalId() {
return `${deleteEscalationPolicyModalId}-${this.policy.id}`;
},
}, },
}; };
</script> </script>
<template> <template>
<div>
<gl-card <gl-card
class="gl-mt-5" class="gl-mt-5"
:class="{ 'gl-border-bottom-0': !isPolicyVisible }" :class="{ 'gl-border-bottom-0': !isPolicyVisible }"
...@@ -102,11 +113,11 @@ export default { ...@@ -102,11 +113,11 @@ export default {
disabled disabled
/> />
<gl-button <gl-button
v-gl-modal="deletePolicyModalId"
v-gl-tooltip v-gl-tooltip
:title="$options.i18n.deletePolicy" :title="$options.i18n.deletePolicy"
icon="remove"
:aria-label="$options.i18n.deletePolicy" :aria-label="$options.i18n.deletePolicy"
disabled icon="remove"
/> />
</gl-button-group> </gl-button-group>
</div> </div>
...@@ -150,4 +161,7 @@ export default { ...@@ -150,4 +161,7 @@ export default {
</div> </div>
</gl-collapse> </gl-collapse>
</gl-card> </gl-card>
<delete-escalation-policy-modal :escalation-policy="policy" :modal-id="deletePolicyModalId" />
</div>
</template> </template>
...@@ -20,3 +20,4 @@ export const DEFAULT_ESCALATION_RULE = { ...@@ -20,3 +20,4 @@ export const DEFAULT_ESCALATION_RULE = {
export const addEscalationPolicyModalId = 'addEscalationPolicyModal'; export const addEscalationPolicyModalId = 'addEscalationPolicyModal';
export const editEscalationPolicyModalId = 'editEscalationPolicyModal'; export const editEscalationPolicyModalId = 'editEscalationPolicyModal';
export const deleteEscalationPolicyModalId = 'deleteEscalationPolicyModal';
import produce from 'immer'; import produce from 'immer';
import createFlash from '~/flash';
import { s__ } from '~/locale';
export const DELETE_ESCALATION_POLICY_ERROR = s__(
'EscalationPolicies|The escalation policy could not be deleted. Please try again.',
);
const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, variables) => { const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, variables) => {
const policy = escalationPolicyCreate?.escalationPolicy; const policy = escalationPolicyCreate?.escalationPolicy;
...@@ -22,10 +29,47 @@ const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, va ...@@ -22,10 +29,47 @@ const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, va
}); });
}; };
const deleteEscalationPolicFromStore = (store, query, { escalationPolicyDestroy }, variables) => {
const escalationPolicy = escalationPolicyDestroy?.escalationPolicy;
if (!escalationPolicy) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
draftData.project.incidentManagementEscalationPolicies.nodes = draftData.project.incidentManagementEscalationPolicies.nodes.filter(
({ id }) => id !== escalationPolicy.id,
);
});
store.writeQuery({
query,
variables,
data,
});
};
export const hasErrors = ({ errors = [] }) => errors?.length; export const hasErrors = ({ errors = [] }) => errors?.length;
const onError = (data, message) => {
createFlash({ message });
throw new Error(data.errors);
};
export const updateStoreOnEscalationPolicyCreate = (store, query, data, variables) => { export const updateStoreOnEscalationPolicyCreate = (store, query, data, variables) => {
if (!hasErrors(data)) { if (!hasErrors(data)) {
addEscalationPolicyToStore(store, query, data, variables); addEscalationPolicyToStore(store, query, data, variables);
} }
}; };
export const updateStoreAfterEscalationPolicyDelete = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, DELETE_ESCALATION_POLICY_ERROR);
} else {
deleteEscalationPolicFromStore(store, query, data, variables);
}
};
#import "../fragments/escalation_policy.fragment.graphql"
mutation DestroyEscalationPolicy($input: EscalationPolicyDestroyInput!) {
escalationPolicyDestroy(input: $input) {
escalationPolicy {
...EscalationPolicy
}
errors
}
}
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EscalationPolicy renders policy with rules 1`] = ` exports[`EscalationPolicy renders policy with rules 1`] = `
<gl-card-stub <div>
<gl-card-stub
bodyclass="[object Object]" bodyclass="[object Object]"
class="gl-mt-5" class="gl-mt-5"
footerclass="" footerclass=""
headerclass="[object Object]" headerclass="[object Object]"
> >
<gl-collapse-stub <gl-collapse-stub
visible="true" visible="true"
...@@ -110,5 +111,11 @@ exports[`EscalationPolicy renders policy with rules 1`] = ` ...@@ -110,5 +111,11 @@ exports[`EscalationPolicy renders policy with rules 1`] = `
</div> </div>
</div> </div>
</gl-collapse-stub> </gl-collapse-stub>
</gl-card-stub> </gl-card-stub>
<delete-escalation-policy-modal-stub
escalationpolicy="[object Object]"
modalid="deleteEscalationPolicyModal-37"
/>
</div>
`; `;
import { GlModal, GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import VueApollo from 'vue-apollo';
import DeleteEscalationPolicyModal, {
i18n,
} from 'ee/escalation_policies/components/delete_escalation_policy_modal.vue';
import { deleteEscalationPolicyModalId } from 'ee/escalation_policies/constants';
import destroyEscalationPolicyMutation from 'ee/escalation_policies/graphql/mutations/destroy_escalatiion_policy.mutation.graphql';
import getEscalationPoliciesQuery from 'ee/escalation_policies/graphql/queries/get_escalation_policies.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
destroyPolicyResponse,
destroyPolicyResponseWithErrors,
getEscalationPoliciesQueryResponse,
} from './mocks/apollo_mock';
import mockPolicies from './mocks/mockPolicies.json';
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
const localVue = createLocalVue();
describe('DeleteEscalationPolicyModal', () => {
let wrapper;
let fakeApollo;
let destroyPolicyHandler;
const escalationPolicy = cloneDeep(mockPolicies[0]);
const cachedPolicy =
getEscalationPoliciesQueryResponse.data.project.incidentManagementEscalationPolicies.nodes[0];
const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(DeleteEscalationPolicyModal, {
data() {
return {
...data,
};
},
propsData: {
modalId: deleteEscalationPolicyModalId,
escalationPolicy,
...props,
},
provide: {
projectPath,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: { GlSprintf },
});
wrapper.vm.$refs.deleteEscalationPolicyModal.hide = mockHideModal;
};
function createComponentWithApollo({
destroyHandler = jest.fn().mockResolvedValue(destroyPolicyResponse),
} = {}) {
localVue.use(VueApollo);
destroyPolicyHandler = destroyHandler;
const requestHandlers = [
[getEscalationPoliciesQuery, jest.fn().mockResolvedValue(getEscalationPoliciesQueryResponse)],
[destroyEscalationPolicyMutation, destroyPolicyHandler],
];
fakeApollo = createMockApollo(requestHandlers);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: getEscalationPoliciesQuery,
variables: {
projectPath: 'group/project',
},
data: getEscalationPoliciesQueryResponse.data,
});
wrapper = shallowMount(DeleteEscalationPolicyModal, {
localVue,
apolloProvider: fakeApollo,
propsData: {
escalationPolicy: cachedPolicy,
modalId: deleteEscalationPolicyModalId,
},
provide: {
projectPath,
},
stubs: {
GlSprintf,
},
});
}
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
async function awaitApolloDomMock() {
await wrapper.vm.$nextTick(); // kick off the DOM update
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update
}
async function deleteEscalationPolicy(localWrapper) {
localWrapper.findComponent(GlModal).vm.$emit('primary', { preventDefault: jest.fn() });
}
afterEach(() => {
wrapper.destroy();
});
describe('layout', () => {
beforeEach(() => {
createComponent();
});
it('sets correct `modalId`', () => {
expect(findModal().props('modalId')).toBe(deleteEscalationPolicyModalId);
});
it('renders the confirmation message with provided policy name', () => {
expect(wrapper.text()).toBe(
i18n.deleteEscalationPolicyMessage.replace('%{escalationPolicy}', escalationPolicy.name),
);
});
});
describe('actions', () => {
beforeEach(() => {
createComponent();
});
it('makes a request to delete an escalation policy on delete confirmation', () => {
mutate.mockResolvedValueOnce({});
deleteEscalationPolicy(wrapper);
expect(mutate).toHaveBeenCalledWith({
mutation: destroyEscalationPolicyMutation,
update: expect.any(Function),
variables: { input: { id: escalationPolicy.id } },
});
});
it('hides the modal on successful escalation policy deletion', async () => {
mutate.mockResolvedValueOnce({ data: { escalationPolicyDestroy: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide the modal and shows an error alert on deletion fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { escalationPolicyDestroy: { errors: [error] } } });
deleteEscalationPolicy(wrapper);
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
describe('with mocked Apollo client', () => {
it('has the name of the escalation policy to delete based on `getEscalationPoliciesQuery` response', async () => {
createComponentWithApollo();
await jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(findModal().text()).toContain(cachedPolicy.name);
});
it('calls a mutation with correct parameters to a policy', async () => {
createComponentWithApollo();
await deleteEscalationPolicy(wrapper);
expect(destroyPolicyHandler).toHaveBeenCalledWith({
input: { id: cachedPolicy.id },
});
});
it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockResolvedValue(destroyPolicyResponseWithErrors),
});
await deleteEscalationPolicy(wrapper);
await awaitApolloDomMock();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(
destroyPolicyResponseWithErrors.data.escalationPolicyDestroy.errors[0],
);
});
});
});
export const getEscalationPoliciesQueryResponse = {
data: {
project: {
incidentManagementEscalationPolicies: {
nodes: [
{
__typename: 'EscalationPolicyType',
id: 'gid://gitlab/IncidentManagement::EscalationPolicy/25',
name: 'Policy',
description: 'Monitor policy description',
rules: [
{
id: 'gid://gitlab/IncidentManagement::EscalationRule/35',
status: 'ACKNOWLEDGED',
elapsedTimeSeconds: 60,
oncallSchedule: {
iid: '1',
name: 'Schedule',
__typename: 'IncidentManagementOncallSchedule',
},
__typename: 'EscalationRuleType',
},
],
},
],
},
},
},
};
export const destroyPolicyResponse = {
data: {
escalationPolicyDestroy: {
escalationPolicy: {
__typename: 'EscalationPolicyType',
id: 'gid://gitlab/IncidentManagement::EscalationPolicy/25',
name: 'Policy',
description: 'Monitor policy description',
rules: [],
},
errors: [],
__typename: 'EscalationPolicyDestroyPayload',
},
},
};
export const destroyPolicyResponseWithErrors = {
data: {
escalationPolicyDestroy: {
escalationPolicy: {
__typename: 'EscalationPolicyType',
id: 'gid://gitlab/IncidentManagement::EscalationPolicy/25',
name: 'Policy',
description: 'Monitor policy description',
rules: [],
},
errors: ['Ooh, somethigb went wrong!'],
__typename: 'EscalationPolicyDestroyPayload',
},
},
};
...@@ -13045,6 +13045,9 @@ msgstr "" ...@@ -13045,6 +13045,9 @@ msgstr ""
msgid "EscalationPolicies|Add policy" msgid "EscalationPolicies|Add policy"
msgstr "" msgstr ""
msgid "EscalationPolicies|Are you sure you want to delete the \"%{escalationPolicy}\" escalation policy? This action cannot be undone."
msgstr ""
msgid "EscalationPolicies|Create an escalation policy in GitLab" msgid "EscalationPolicies|Create an escalation policy in GitLab"
msgstr "" msgstr ""
...@@ -13087,6 +13090,9 @@ msgstr "" ...@@ -13087,6 +13090,9 @@ msgstr ""
msgid "EscalationPolicies|THEN %{doAction} %{schedule}" msgid "EscalationPolicies|THEN %{doAction} %{schedule}"
msgstr "" msgstr ""
msgid "EscalationPolicies|The escalation policy could not be deleted. Please try again."
msgstr ""
msgid "EscalationPolicies|mins" msgid "EscalationPolicies|mins"
msgstr "" msgstr ""
......
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