Commit 99a8861f authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '268356-add-escalation-pollicies-modal-integration' into 'master'

Add escalation policy BE integration

See merge request gitlab-org/gitlab!61815
parents 5e6b0b37 01673739
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
width: 640px; width: 640px;
} }
.escalation-policy-rules { .rule-control {
.rule-control {
width: 240px; width: 240px;
} }
.rule-elapsed-minutes { .rule-elapsed-minutes {
width: 56px; width: 56px;
}
} }
.rule-close-icon {
right: 1rem;
}
<script> <script>
import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import createFlash from '~/flash';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { defaultEscalationRule } from '../constants'; import { defaultEscalationRule } from '../constants';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import EscalationRule from './escalation_rule.vue'; import EscalationRule from './escalation_rule.vue';
export const i18n = { export const i18n = {
...@@ -19,6 +21,7 @@ export const i18n = { ...@@ -19,6 +21,7 @@ export const i18n = {
}, },
}, },
addRule: s__('EscalationPolicies|+ Add an additional rule'), addRule: s__('EscalationPolicies|+ Add an additional rule'),
failedLoadingSchedules: s__('EscalationPolicies|Failed to load oncall-schedules'),
}; };
export default { export default {
...@@ -30,6 +33,7 @@ export default { ...@@ -30,6 +33,7 @@ export default {
GlFormInput, GlFormInput,
EscalationRule, EscalationRule,
}, },
inject: ['projectPath'],
props: { props: {
form: { form: {
type: Object, type: Object,
...@@ -42,12 +46,50 @@ export default { ...@@ -42,12 +46,50 @@ export default {
}, },
data() { data() {
return { return {
rules: [cloneDeep(defaultEscalationRule)], schedules: [],
rules: [],
uid: 0,
}; };
}, },
apollo: {
schedules: {
query: getOncallSchedulesQuery,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
const nodes = data.project?.incidentManagementOncallSchedules?.nodes ?? [];
return nodes;
},
error(error) {
createFlash({ message: i18n.failedLoadingSchedules, captureError: true, error });
},
},
},
mounted() {
this.rules.push({ ...cloneDeep(defaultEscalationRule), key: this.getUid() });
},
methods: { methods: {
addRule() { addRule() {
this.rules.push(cloneDeep(defaultEscalationRule)); this.rules.push({ ...cloneDeep(defaultEscalationRule), key: this.getUid() });
this.emitUpdate();
},
updateEscalationRules(index, rule) {
this.rules[index] = rule;
this.emitUpdate();
},
removeEscalationRule(index) {
this.rules.splice(index, 1);
this.emitUpdate();
},
emitUpdate() {
this.$emit('update-escalation-policy-form', { field: 'rules', value: this.rules });
},
getUid() {
this.uid += 1;
return this.uid;
}, },
}, },
}; };
...@@ -92,13 +134,17 @@ export default { ...@@ -92,13 +134,17 @@ export default {
</gl-form-group> </gl-form-group>
</div> </div>
<gl-form-group <gl-form-group class="gl-mb-3" :label="$options.i18n.fields.rules.title" label-size="sm">
class="escalation-policy-rules" <escalation-rule
:label="$options.i18n.fields.rules.title" v-for="(rule, index) in rules"
label-size="sm" :key="rule.key"
:state="validationState.rules" :rule="rule"
> :index="index"
<escalation-rule v-for="(rule, index) in rules" :key="index" :rule="rule" /> :schedules="schedules"
:is-valid="validationState.rules[index]"
@update-escalation-rule="updateEscalationRules"
@remove-escalation-rule="removeEscalationRule"
/>
</gl-form-group> </gl-form-group>
<gl-link @click="addRule"> <gl-link @click="addRule">
<span>{{ $options.i18n.addRule }}</span> <span>{{ $options.i18n.addRule }}</span>
......
<script> <script>
import { GlModal } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { set } from 'lodash'; import { set } from 'lodash';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { addEscalationPolicyModalId } from '../constants'; import { addEscalationPolicyModalId } from '../constants';
import { isNameFieldValid } from '../utils'; import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql';
import { isNameFieldValid, getRulesValidationState } from '../utils';
import AddEditEscalationPolicyForm from './add_edit_escalation_policy_form.vue'; import AddEditEscalationPolicyForm from './add_edit_escalation_policy_form.vue';
export const i18n = { export const i18n = {
...@@ -17,8 +18,10 @@ export default { ...@@ -17,8 +18,10 @@ export default {
addEscalationPolicyModalId, addEscalationPolicyModalId,
components: { components: {
GlModal, GlModal,
GlAlert,
AddEditEscalationPolicyForm, AddEditEscalationPolicyForm,
}, },
inject: ['projectPath'],
props: { props: {
escalationPolicy: { escalationPolicy: {
type: Object, type: Object,
...@@ -32,11 +35,13 @@ export default { ...@@ -32,11 +35,13 @@ export default {
form: { form: {
name: this.escalationPolicy.name, name: this.escalationPolicy.name,
description: this.escalationPolicy.description, description: this.escalationPolicy.description,
rules: [],
}, },
validationState: { validationState: {
name: true, name: true,
rules: true, rules: [],
}, },
error: null,
}; };
}, },
computed: { computed: {
...@@ -56,7 +61,15 @@ export default { ...@@ -56,7 +61,15 @@ export default {
}; };
}, },
isFormValid() { isFormValid() {
return Object.values(this.validationState).every(Boolean); return this.validationState.name && this.validationState.rules.every(Boolean);
},
serializedData() {
const rules = this.form.rules.map(({ status, elapsedTimeSeconds, oncallScheduleIid }) => ({
status,
elapsedTimeSeconds,
oncallScheduleIid,
}));
return { ...this.form, rules };
}, },
}, },
methods: { methods: {
...@@ -64,10 +77,59 @@ export default { ...@@ -64,10 +77,59 @@ export default {
set(this.form, field, value); set(this.form, field, value);
this.validateForm(field); this.validateForm(field);
}, },
createEscalationPolicy() {
this.loading = true;
const { projectPath } = this;
this.$apollo
.mutate({
mutation: createEscalationPolicyMutation,
variables: {
input: {
projectPath,
...this.serializedData,
},
},
})
.then(
({
data: {
escalationPolicyCreate: {
errors: [error],
},
},
}) => {
if (error) {
throw error;
}
this.$refs.addUpdateEscalationPolicyModal.hide();
this.$emit('policyCreated');
this.clearForm();
},
)
.catch((error) => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
validateForm(field) { validateForm(field) {
if (field === 'name') { if (field === 'name') {
this.validationState.name = isNameFieldValid(this.form.name); this.validationState.name = isNameFieldValid(this.form.name);
} }
if (field === 'rules') {
this.validationState.rules = getRulesValidationState(this.form.rules);
}
},
hideErrorAlert() {
this.error = null;
},
clearForm() {
this.form = {
name: '',
description: '',
rules: [],
};
}, },
}, },
}; };
...@@ -75,12 +137,18 @@ export default { ...@@ -75,12 +137,18 @@ export default {
<template> <template>
<gl-modal <gl-modal
ref="addUpdateEscalationPolicyModal"
class="escalation-policy-modal" class="escalation-policy-modal"
:modal-id="$options.addEscalationPolicyModalId" :modal-id="$options.addEscalationPolicyModalId"
:title="$options.i18n.addEscalationPolicy" :title="$options.i18n.addEscalationPolicy"
:action-primary="actionsProps.primary" :action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel" :action-cancel="actionsProps.cancel"
@primary.prevent="createEscalationPolicy"
@cancel="clearForm"
> >
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error }}
</gl-alert>
<add-edit-escalation-policy-form <add-edit-escalation-policy-form
:validation-state="validationState" :validation-state="validationState"
:form="form" :form="form"
......
<script> <script>
import { GlFormInput, GlDropdown, GlDropdownItem, GlCard, GlSprintf } from '@gitlab/ui'; import {
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlCard,
GlIcon,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { ACTIONS, ALERT_STATUSES } from '../constants'; import { ACTIONS, ALERT_STATUSES } from '../constants';
...@@ -9,6 +17,9 @@ export const i18n = { ...@@ -9,6 +17,9 @@ export const i18n = {
condition: s__('EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes'), condition: s__('EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes'),
action: s__('EscalationPolicies|THEN %{doAction} %{schedule}'), action: s__('EscalationPolicies|THEN %{doAction} %{schedule}'),
selectSchedule: s__('EscalationPolicies|Select schedule'), selectSchedule: s__('EscalationPolicies|Select schedule'),
validationMsg: s__(
'EscalationPolicies|A schedule is required for adding an escalation policy.',
),
}, },
}, },
}; };
...@@ -18,10 +29,12 @@ export default { ...@@ -18,10 +29,12 @@ export default {
ALERT_STATUSES, ALERT_STATUSES,
ACTIONS, ACTIONS,
components: { components: {
GlFormGroup,
GlFormInput, GlFormInput,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlCard, GlCard,
GlIcon,
GlSprintf, GlSprintf,
}, },
props: { props: {
...@@ -34,32 +47,93 @@ export default { ...@@ -34,32 +47,93 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
index: {
type: Number,
required: true,
},
isValid: {
type: Boolean,
required: false,
default: true,
},
},
data() {
const { status, elapsedTimeSeconds, action, oncallScheduleIid } = this.rule;
return {
status,
elapsedTimeSeconds,
action,
oncallScheduleIid,
};
},
computed: {
scheduleDropdownTitle() {
return this.oncallScheduleIid
? this.schedules.find(({ iid }) => iid === this.oncallScheduleIid)?.name
: i18n.fields.rules.selectSchedule;
},
},
methods: {
setOncallSchedule({ iid }) {
this.oncallScheduleIid = this.oncallScheduleIid === iid ? null : iid;
this.emitUpdate();
},
setStatus(status) {
this.status = status;
this.emitUpdate();
},
emitUpdate() {
this.$emit('update-escalation-rule', this.index, {
oncallScheduleIid: parseInt(this.oncallScheduleIid, 10),
action: this.action,
status: this.status,
elapsedTimeSeconds: parseInt(this.elapsedTimeSeconds, 10),
});
},
}, },
}; };
</script> </script>
<template> <template>
<gl-card class="gl-border-gray-400 gl-bg-gray-10 gl-mb-3"> <gl-card class="gl-border-gray-400 gl-bg-gray-10 gl-mb-3 gl-relative">
<gl-icon
v-if="index !== 0"
name="close"
class="gl-absolute rule-close-icon"
@click="$emit('remove-escalation-rule', index)"
/>
<gl-form-group
:invalid-feedback="$options.i18n.fields.rules.validationMsg"
:state="isValid"
class="gl-mb-0"
>
<div class="gl-display-flex gl-align-items-center"> <div class="gl-display-flex gl-align-items-center">
<gl-sprintf :message="$options.i18n.fields.rules.condition"> <gl-sprintf :message="$options.i18n.fields.rules.condition">
<template #alertStatus> <template #alertStatus>
<gl-dropdown <gl-dropdown
class="rule-control gl-mx-3" class="rule-control gl-mx-3"
:text="$options.ALERT_STATUSES[rule.status]" :text="$options.ALERT_STATUSES[status]"
data-testid="alert-status-dropdown" data-testid="alert-status-dropdown"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="(label, status) in $options.ALERT_STATUSES" v-for="(label, alertStatus) in $options.ALERT_STATUSES"
:key="status" :key="alertStatus"
:is-checked="rule.status === status" :is-checked="status === alertStatus"
is-check-item is-check-item
@click="setStatus(alertStatus)"
> >
{{ label }} {{ label }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
<template #minutes> <template #minutes>
<gl-form-input class="gl-mx-3 rule-elapsed-minutes" :value="0" /> <gl-form-input
v-model="elapsedTimeSeconds"
class="gl-mx-3 gl-inset-border-1-gray-200! rule-elapsed-minutes"
type="number"
min="0"
@change="emitUpdate"
/>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
...@@ -72,9 +146,9 @@ export default { ...@@ -72,9 +146,9 @@ export default {
data-testid="action-dropdown" data-testid="action-dropdown"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="(label, action) in $options.ACTIONS" v-for="(label, ruleAction) in $options.ACTIONS"
:key="action" :key="ruleAction"
:is-checked="rule.action === action" :is-checked="rule.action === ruleAction"
is-check-item is-check-item
> >
{{ label }} {{ label }}
...@@ -83,16 +157,28 @@ export default { ...@@ -83,16 +157,28 @@ export default {
</template> </template>
<template #schedule> <template #schedule>
<gl-dropdown <gl-dropdown
class="rule-control gl-mx-3" class="rule-control"
:text="$options.i18n.fields.rules.selectSchedule" :text="scheduleDropdownTitle"
data-testid="schedules-dropdown" data-testid="schedules-dropdown"
> >
<gl-dropdown-item v-for="schedule in schedules" :key="schedule.id" is-check-item> <template #button-text>
<span :class="{ 'gl-text-gray-400': !oncallScheduleIid }">
{{ scheduleDropdownTitle }}
</span>
</template>
<gl-dropdown-item
v-for="schedule in schedules"
:key="schedule.iid"
:is-checked="schedule.iid === oncallScheduleIid"
is-check-item
@click="setOncallSchedule(schedule)"
>
{{ schedule.name }} {{ schedule.name }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
</gl-form-group>
</gl-card> </gl-card>
</template> </template>
...@@ -13,10 +13,7 @@ export const defaultEscalationRule = { ...@@ -13,10 +13,7 @@ export const defaultEscalationRule = {
status: 'ACKNOWLEDGED', status: 'ACKNOWLEDGED',
elapsedTimeSeconds: 0, elapsedTimeSeconds: 0,
action: 'EMAIL_ONCALL_SCHEDULE_USER', action: 'EMAIL_ONCALL_SCHEDULE_USER',
oncallSchedule: { oncallScheduleIid: null,
iid: null,
name: null,
},
}; };
export const addEscalationPolicyModalId = 'addEscalationPolicyModal'; export const addEscalationPolicyModalId = 'addEscalationPolicyModal';
mutation escalationPolicyCreate($input: EscalationPolicyCreateInput!) {
escalationPolicyCreate(input: $input) {
escalationPolicy {
id
name
description
rules {
status
elapsedTimeSeconds
oncallSchedule {
iid
name
}
}
}
errors
}
}
query getOncallSchedules($projectPath: ID!) {
project(fullPath: $projectPath) {
incidentManagementOncallSchedules {
nodes {
iid
name
}
}
}
}
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import EscalationPoliciesWrapper from './components/escalation_policies_wrapper.vue'; import EscalationPoliciesWrapper from './components/escalation_policies_wrapper.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
dataIdFromObject: (object) => {
// eslint-disable-next-line no-underscore-dangle
if (object.__typename === 'IncidentManagementOncallSchedule') {
return object.iid;
}
return defaultDataIdFromObject(object);
},
},
},
),
});
export default () => { export default () => {
const el = document.querySelector('.js-escalation-policies'); const el = document.querySelector('.js-escalation-policies');
...@@ -10,6 +32,7 @@ export default () => { ...@@ -10,6 +32,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
apolloProvider,
provide: { provide: {
projectPath, projectPath,
emptyEscalationPoliciesSvgPath, emptyEscalationPoliciesSvgPath,
......
...@@ -7,3 +7,13 @@ ...@@ -7,3 +7,13 @@
export const isNameFieldValid = (name) => { export const isNameFieldValid = (name) => {
return Boolean(name?.length); return Boolean(name?.length);
}; };
/**
* Returns an array of booleans - validation state for each rule
* @param {Array} rules
*
* @returns {Array}
*/
export const getRulesValidationState = (rules) => {
return rules.map((rule) => Boolean(rule.oncallScheduleIid));
};
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
module IncidentManagement module IncidentManagement
module EscalationPolicyHelper module EscalationPolicyHelper
def escalation_policy_data def escalation_policy_data(project)
{ {
'project-path' => project.full_path,
'empty_escalation_policies_svg_path' => image_path('illustrations/empty-state/empty-escalation.svg') 'empty_escalation_policies_svg_path' => image_path('illustrations/empty-state/empty-escalation.svg')
} }
end end
......
- page_title _('Escalation policies') - page_title _('Escalation policies')
- add_page_specific_style 'page_bundles/escalation_policies' - add_page_specific_style 'page_bundles/escalation_policies'
.js-escalation-policies{ data: escalation_policy_data } .js-escalation-policies{ data: escalation_policy_data(@project) }
...@@ -38,7 +38,7 @@ module EE ...@@ -38,7 +38,7 @@ module EE
end end
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Escalation policies'), title: _('Escalation Policies'),
link: project_incident_management_escalation_policies_path(context.project), link: project_incident_management_escalation_policies_path(context.project),
active_routes: { controller: :escalation_policies }, active_routes: { controller: :escalation_policies },
item_id: :escalation_policies item_id: :escalation_policies
......
...@@ -6,7 +6,6 @@ import AddEscalationPolicyForm, { ...@@ -6,7 +6,6 @@ import AddEscalationPolicyForm, {
import EscalationRule from 'ee/escalation_policies/components/escalation_rule.vue'; import EscalationRule from 'ee/escalation_policies/components/escalation_rule.vue';
import { defaultEscalationRule } from 'ee/escalation_policies/constants'; import { defaultEscalationRule } from 'ee/escalation_policies/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockPolicy from './mocks/mockPolicy.json'; import mockPolicy from './mocks/mockPolicy.json';
describe('AddEscalationPolicyForm', () => { describe('AddEscalationPolicyForm', () => {
...@@ -23,6 +22,7 @@ describe('AddEscalationPolicyForm', () => { ...@@ -23,6 +22,7 @@ describe('AddEscalationPolicyForm', () => {
}, },
validationState: { validationState: {
name: true, name: true,
rules: [],
}, },
...props, ...props,
}, },
...@@ -46,13 +46,15 @@ describe('AddEscalationPolicyForm', () => { ...@@ -46,13 +46,15 @@ describe('AddEscalationPolicyForm', () => {
const findAddRuleLink = () => wrapper.findComponent(GlLink); const findAddRuleLink = () => wrapper.findComponent(GlLink);
describe('Escalation policy form validation', () => { describe('Escalation policy form validation', () => {
it('should show feedback for an invalid name input validation state', async () => { it('should set correct validation state for validated controls', async () => {
createComponent({ createComponent({
props: { props: {
validationState: { name: false }, validationState: { name: false, rules: [false] },
}, },
}); });
expect(findPolicyName().attributes('state')).toBeFalsy(); await wrapper.vm.$nextTick();
expect(findPolicyName().attributes('state')).toBeUndefined();
expect(findRules().at(0).attributes('is-valid')).toBeUndefined();
}); });
}); });
...@@ -72,7 +74,40 @@ describe('AddEscalationPolicyForm', () => { ...@@ -72,7 +74,40 @@ describe('AddEscalationPolicyForm', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
const rules = findRules(); const rules = findRules();
expect(rules.length).toBe(2); expect(rules.length).toBe(2);
expect(rules.at(1).props('rule')).toEqual(defaultEscalationRule); expect(rules.at(1).props('rule')).toMatchObject(defaultEscalationRule);
});
it('should emit updates when rule is added', async () => {
findAddRuleLink().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('update-escalation-policy-form')[0]).toMatchObject([
{
field: 'rules',
value: [
expect.objectContaining(defaultEscalationRule),
expect.objectContaining(defaultEscalationRule),
],
},
]);
});
it('on rule update emitted should update rules array and emit updates up', () => {
const updatedRule = {
status: 'TRIGGERED',
elapsedTimeSeconds: 30,
oncallScheduleIid: 2,
};
findRules().at(0).vm.$emit('update-escalation-rule', 0, updatedRule);
expect(wrapper.emitted('update-escalation-policy-form')[0]).toEqual([
{ field: 'rules', value: [updatedRule] },
]);
});
it('on rule removal emitted should update rules array and emit updates up', () => {
findRules().at(0).vm.$emit('remove-escalation-rule', 0);
expect(wrapper.emitted('update-escalation-policy-form')[0]).toEqual([
{ field: 'rules', value: [] },
]);
}); });
}); });
}); });
import { GlModal } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AddEscalationPolicyForm from 'ee/escalation_policies/components/add_edit_escalation_policy_form.vue'; import AddEscalationPolicyForm from 'ee/escalation_policies/components/add_edit_escalation_policy_form.vue';
import AddEscalationPolicyModal, { import AddEscalationPolicyModal, {
i18n, i18n,
} from 'ee/escalation_policies/components/add_edit_escalation_policy_modal.vue'; } from 'ee/escalation_policies/components/add_edit_escalation_policy_modal.vue';
import waitForPromises from 'helpers/wait_for_promises';
import mockPolicy from './mocks/mockPolicy.json';
describe('AddEscalationPolicyModal', () => { describe('AddEscalationPolicyModal', () => {
let wrapper; let wrapper;
const projectPath = 'group/project'; const projectPath = 'group/project';
const mockHideModal = jest.fn();
const mutate = jest.fn();
const createComponent = ({ escalationPolicy, data } = {}) => { const createComponent = ({ escalationPolicy, data } = {}) => {
wrapper = shallowMount(AddEscalationPolicyModal, { wrapper = shallowMount(AddEscalationPolicyModal, {
data() { data() {
return { return {
form: mockPolicy,
...data, ...data,
}; };
}, },
...@@ -22,7 +27,14 @@ describe('AddEscalationPolicyModal', () => { ...@@ -22,7 +27,14 @@ describe('AddEscalationPolicyModal', () => {
provide: { provide: {
projectPath, projectPath,
}, },
mocks: {
$apollo: {
mutate,
},
},
}); });
wrapper.vm.$refs.addUpdateEscalationPolicyModal.hide = mockHideModal;
}; };
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
...@@ -34,6 +46,7 @@ describe('AddEscalationPolicyModal', () => { ...@@ -34,6 +46,7 @@ describe('AddEscalationPolicyModal', () => {
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findEscalationPolicyForm = () => wrapper.findComponent(AddEscalationPolicyForm); const findEscalationPolicyForm = () => wrapper.findComponent(AddEscalationPolicyForm);
const findAlert = () => wrapper.findComponent(GlAlert);
describe('renders create modal with the correct information', () => { describe('renders create modal with the correct information', () => {
it('renders modal title', () => { it('renders modal title', () => {
...@@ -43,6 +56,49 @@ describe('AddEscalationPolicyModal', () => { ...@@ -43,6 +56,49 @@ describe('AddEscalationPolicyModal', () => {
it('renders the form inside the modal', () => { it('renders the form inside the modal', () => {
expect(findEscalationPolicyForm().exists()).toBe(true); expect(findEscalationPolicyForm().exists()).toBe(true);
}); });
it('makes a request with form data to create an escalation policy', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
projectPath,
...mockPolicy,
},
},
}),
);
});
it('hides the modal on successful policy creation', async () => {
mutate.mockResolvedValueOnce({ data: { escalationPolicyCreate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide a modal and shows error alert on creation failure", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { escalationPolicyCreate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
it('clears the form on modal close', () => {
expect(wrapper.vm.form).toEqual(mockPolicy);
findModal().vm.$emit('cancel', { preventDefault: jest.fn() });
expect(wrapper.vm.form).toEqual({
name: '',
description: '',
rules: [],
});
});
}); });
describe('modal buttons', () => { describe('modal buttons', () => {
......
import { GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import EscalationRule from 'ee/escalation_policies/components/escalation_rule.vue'; import EscalationRule from 'ee/escalation_policies/components/escalation_rule.vue';
...@@ -19,6 +19,8 @@ describe('EscalationRule', () => { ...@@ -19,6 +19,8 @@ describe('EscalationRule', () => {
propsData: { propsData: {
rule: cloneDeep(defaultEscalationRule), rule: cloneDeep(defaultEscalationRule),
schedules: mockSchedules, schedules: mockSchedules,
index: 0,
isValid: false,
...props, ...props,
}, },
stubs: { stubs: {
...@@ -45,6 +47,8 @@ describe('EscalationRule', () => { ...@@ -45,6 +47,8 @@ describe('EscalationRule', () => {
const findSchedulesDropdown = () => wrapper.findByTestId('schedules-dropdown'); const findSchedulesDropdown = () => wrapper.findByTestId('schedules-dropdown');
const findSchedulesDropdownOptions = () => findSchedulesDropdown().findAll(GlDropdownItem); const findSchedulesDropdownOptions = () => findSchedulesDropdown().findAll(GlDropdownItem);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
describe('Status dropdown', () => { describe('Status dropdown', () => {
it('should have correct alert status options', () => { it('should have correct alert status options', () => {
expect(findStatusDropdownOptions().wrappers.map((w) => w.text())).toStrictEqual( expect(findStatusDropdownOptions().wrappers.map((w) => w.text())).toStrictEqual(
...@@ -76,4 +80,19 @@ describe('EscalationRule', () => { ...@@ -76,4 +80,19 @@ describe('EscalationRule', () => {
); );
}); });
}); });
describe('Validation', () => {
it.each`
isValid | state
${true} | ${'true'}
${false} | ${undefined}
`('when $isValid sets from group state to $state', ({ isValid, state }) => {
createComponent({
props: {
isValid,
},
});
expect(findFormGroup().attributes('state')).toBe(state);
});
});
}); });
...@@ -3,11 +3,14 @@ ...@@ -3,11 +3,14 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IncidentManagement::EscalationPolicyHelper do RSpec.describe IncidentManagement::EscalationPolicyHelper do
let_it_be(:project) { create(:project) }
describe '#escalation_policy_data' do describe '#escalation_policy_data' do
subject(:data) { helper.escalation_policy_data } subject(:data) { helper.escalation_policy_data(project) }
it 'returns scalation policies data' do it 'returns scalation policies data' do
is_expected.to eq( is_expected.to eq(
'project-path' => project.full_path,
'empty_escalation_policies_svg_path' => helper.image_path('illustrations/empty-state/empty-escalation.svg') 'empty_escalation_policies_svg_path' => helper.image_path('illustrations/empty-state/empty-escalation.svg')
) )
end end
......
...@@ -26,7 +26,7 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do ...@@ -26,7 +26,7 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do
end end
end end
describe 'Escalation policies' do describe 'Escalation Policies' do
let(:item_id) { :escalation_policies } let(:item_id) { :escalation_policies }
before do before do
......
...@@ -275,7 +275,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do ...@@ -275,7 +275,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end end
end end
describe 'Escalation policies' do describe 'Escalation Policies' do
before do before do
allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_user).and_return(user)
stub_licensed_features(oncall_schedules: true, escalation_policies: true) stub_licensed_features(oncall_schedules: true, escalation_policies: true)
...@@ -284,7 +284,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do ...@@ -284,7 +284,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'has a link to the escalation policies page' do it 'has a link to the escalation policies page' do
render render
expect(rendered).to have_link('Escalation policies', href: project_incident_management_escalation_policies_path(project)) expect(rendered).to have_link('Escalation Policies', href: project_incident_management_escalation_policies_path(project))
end end
describe 'when the user does not have access' do describe 'when the user does not have access' do
...@@ -293,7 +293,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do ...@@ -293,7 +293,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'does not have a link to the escalation policies page' do it 'does not have a link to the escalation policies page' do
render render
expect(rendered).not_to have_link('Escalation policies') expect(rendered).not_to have_link('Escalation Policies')
end end
end end
end end
......
...@@ -13072,6 +13072,9 @@ msgstr "" ...@@ -13072,6 +13072,9 @@ msgstr ""
msgid "Errors:" msgid "Errors:"
msgstr "" msgstr ""
msgid "Escalation Policies"
msgstr ""
msgid "Escalation policies" msgid "Escalation policies"
msgstr "" msgstr ""
...@@ -13081,6 +13084,9 @@ msgstr "" ...@@ -13081,6 +13084,9 @@ msgstr ""
msgid "EscalationPolicies|+ Add an additional rule" msgid "EscalationPolicies|+ Add an additional rule"
msgstr "" msgstr ""
msgid "EscalationPolicies|A schedule is required for adding an escalation policy."
msgstr ""
msgid "EscalationPolicies|Add an escalation policy" msgid "EscalationPolicies|Add an escalation policy"
msgstr "" msgstr ""
...@@ -13099,6 +13105,9 @@ msgstr "" ...@@ -13099,6 +13105,9 @@ msgstr ""
msgid "EscalationPolicies|Escalation rules" msgid "EscalationPolicies|Escalation rules"
msgstr "" msgstr ""
msgid "EscalationPolicies|Failed to load oncall-schedules"
msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes" msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes"
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