Commit 1354c674 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Nicolò Maria Mezzopera

Add option to email user in escalation policy

parent de0c2a54
...@@ -3,7 +3,12 @@ import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; ...@@ -3,7 +3,12 @@ import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { cloneDeep, uniqueId } from 'lodash'; import { cloneDeep, uniqueId } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { DEFAULT_ACTION, DEFAULT_ESCALATION_RULE, MAX_RULES_LENGTH } from '../constants'; import {
EMAIL_ONCALL_SCHEDULE_USER,
DEFAULT_ESCALATION_RULE,
EMAIL_USER,
MAX_RULES_LENGTH,
} from '../constants';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql'; import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import EscalationRule from './escalation_rule.vue'; import EscalationRule from './escalation_rule.vue';
...@@ -78,17 +83,14 @@ export default { ...@@ -78,17 +83,14 @@ export default {
}, },
mounted() { mounted() {
this.rules = this.form.rules.map((rule) => { this.rules = this.form.rules.map((rule) => {
const { const { status, elapsedTimeMinutes, oncallSchedule, user } = rule;
status,
elapsedTimeMinutes,
oncallSchedule: { iid: oncallScheduleIid },
} = rule;
return { return {
status, status,
elapsedTimeMinutes, elapsedTimeMinutes,
action: DEFAULT_ACTION, action: user ? EMAIL_USER : EMAIL_ONCALL_SCHEDULE_USER,
oncallScheduleIid, oncallScheduleIid: oncallSchedule?.iid,
username: user?.username,
key: uniqueId(), key: uniqueId(),
}; };
}); });
...@@ -102,7 +104,8 @@ export default { ...@@ -102,7 +104,8 @@ export default {
this.rules.push({ ...cloneDeep(DEFAULT_ESCALATION_RULE), key: uniqueId() }); this.rules.push({ ...cloneDeep(DEFAULT_ESCALATION_RULE), key: uniqueId() });
}, },
updateEscalationRules({ rule, index }) { updateEscalationRules({ rule, index }) {
this.rules[index] = { ...this.rules[index], ...rule }; const { key } = this.rules[index];
this.rules[index] = { key, ...rule };
this.emitRulesUpdate(); this.emitRulesUpdate();
}, },
removeEscalationRule(index) { removeEscalationRule(index) {
......
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql'; import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql';
import updateEscalationPolicyMutation from '../graphql/mutations/update_escalation_policy.mutation.graphql'; import updateEscalationPolicyMutation from '../graphql/mutations/update_escalation_policy.mutation.graphql';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql'; import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import { isNameFieldValid, getRulesValidationState, serializeRule } from '../utils'; import { isNameFieldValid, getRulesValidationState, serializeRule, getRules } 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 = {
...@@ -82,7 +82,8 @@ export default { ...@@ -82,7 +82,8 @@ export default {
this.validationState.name && this.validationState.name &&
(this.isEditMode ? true : this.validationState.rules.length) && (this.isEditMode ? true : this.validationState.rules.length) &&
this.validationState.rules.every( this.validationState.rules.every(
({ isTimeValid, isScheduleValid }) => isTimeValid && isScheduleValid, ({ isTimeValid, isScheduleValid, isUserValid }) =>
isTimeValid && isScheduleValid && isUserValid,
) )
); );
}, },
...@@ -90,12 +91,12 @@ export default { ...@@ -90,12 +91,12 @@ export default {
return ( return (
this.form.name !== this.initialState.name || this.form.name !== this.initialState.name ||
this.form.description !== this.initialState.description || this.form.description !== this.initialState.description ||
!isEqual(this.getRules(this.form.rules), this.getRules(this.initialState.rules)) !isEqual(getRules(this.form.rules), getRules(this.initialState.rules))
); );
}, },
requestParams() { requestParams() {
const id = this.isEditMode ? { id: this.escalationPolicy.id } : {}; const id = this.isEditMode ? { id: this.escalationPolicy.id } : {};
return { ...this.form, ...id, rules: this.getRules(this.form.rules).map(serializeRule) }; return { ...this.form, ...id, rules: getRules(this.form.rules).map(serializeRule) };
}, },
}, },
methods: { methods: {
...@@ -188,15 +189,6 @@ export default { ...@@ -188,15 +189,6 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
getRules(rules) {
return rules.map(
({ status, elapsedTimeMinutes, oncallScheduleIid, oncallSchedule: { iid } = {} }) => ({
status,
elapsedTimeMinutes,
oncallScheduleIid: oncallScheduleIid || iid,
}),
);
},
validateForm(field) { validateForm(field) {
if (field === 'name') { if (field === 'name') {
this.validationState.name = isNameFieldValid(this.form.name); this.validationState.name = isNameFieldValid(this.form.name);
......
...@@ -8,14 +8,17 @@ import { ...@@ -8,14 +8,17 @@ import {
GlSprintf, GlSprintf,
GlIcon, GlIcon,
GlCollapse, GlCollapse,
GlToken,
GlAvatar,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { import {
ACTIONS, ACTIONS,
ALERT_STATUSES, ALERT_STATUSES,
DEFAULT_ACTION, EMAIL_ONCALL_SCHEDULE_USER,
deleteEscalationPolicyModalId, deleteEscalationPolicyModalId,
editEscalationPolicyModalId, editEscalationPolicyModalId,
EMAIL_USER,
} from '../constants'; } from '../constants';
import EditEscalationPolicyModal from './add_edit_escalation_policy_modal.vue'; import EditEscalationPolicyModal from './add_edit_escalation_policy_modal.vue';
import DeleteEscalationPolicyModal from './delete_escalation_policy_modal.vue'; import DeleteEscalationPolicyModal from './delete_escalation_policy_modal.vue';
...@@ -24,22 +27,22 @@ export const i18n = { ...@@ -24,22 +27,22 @@ export const i18n = {
editPolicy: s__('EscalationPolicies|Edit escalation policy'), editPolicy: s__('EscalationPolicies|Edit escalation policy'),
deletePolicy: s__('EscalationPolicies|Delete escalation policy'), deletePolicy: s__('EscalationPolicies|Delete escalation policy'),
escalationRule: s__( escalationRule: s__(
'EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{schedule}', 'EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{scheduleOrUser}',
), ),
minutes: s__('EscalationPolicies|mins'), minutes: s__('EscalationPolicies|mins'),
noRules: s__('EscalationPolicies|This policy has no escalation rules.'), noRules: s__('EscalationPolicies|This policy has no escalation rules.'),
}; };
const isRuleValid = ({ status, elapsedTimeMinutes, oncallSchedule: { name } }) => const isRuleValid = ({ status, elapsedTimeMinutes, oncallSchedule, user }) =>
Object.keys(ALERT_STATUSES).includes(status) && Object.keys(ALERT_STATUSES).includes(status) &&
typeof elapsedTimeMinutes === 'number' && typeof elapsedTimeMinutes === 'number' &&
typeof name === 'string'; (typeof oncallSchedule?.name === 'string' || typeof user?.username === 'string');
export default { export default {
i18n, i18n,
ACTIONS, ACTIONS,
ALERT_STATUSES, ALERT_STATUSES,
DEFAULT_ACTION, EMAIL_ONCALL_SCHEDULE_USER,
components: { components: {
GlButton, GlButton,
GlButtonGroup, GlButtonGroup,
...@@ -47,6 +50,8 @@ export default { ...@@ -47,6 +50,8 @@ export default {
GlSprintf, GlSprintf,
GlIcon, GlIcon,
GlCollapse, GlCollapse,
GlToken,
GlAvatar,
DeleteEscalationPolicyModal, DeleteEscalationPolicyModal,
EditEscalationPolicyModal, EditEscalationPolicyModal,
}, },
...@@ -87,6 +92,20 @@ export default { ...@@ -87,6 +92,20 @@ export default {
return `${deleteEscalationPolicyModalId}-${this.policy.id}`; return `${deleteEscalationPolicyModalId}-${this.policy.id}`;
}, },
}, },
methods: {
hasEscalationSchedule(rule) {
return rule.oncallSchedule?.iid;
},
hasEscalationUser(rule) {
return rule.user?.username;
},
getActionName(rule) {
return (this.hasEscalationSchedule(rule)
? ACTIONS[EMAIL_ONCALL_SCHEDULE_USER]
: ACTIONS[EMAIL_USER]
).toLowerCase();
},
},
}; };
</script> </script>
...@@ -147,6 +166,7 @@ export default { ...@@ -147,6 +166,7 @@ export default {
v-for="(rule, ruleIndex) in policy.rules" v-for="(rule, ruleIndex) in policy.rules"
:key="rule.id" :key="rule.id"
:class="{ 'gl-mb-5': ruleIndex !== policy.rules.length - 1 }" :class="{ 'gl-mb-5': ruleIndex !== policy.rules.length - 1 }"
class="gl-display-flex gl-align-items-center"
> >
<gl-icon name="clock" class="gl-mr-3" /> <gl-icon name="clock" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.escalationRule"> <gl-sprintf :message="$options.i18n.escalationRule">
...@@ -155,7 +175,7 @@ export default { ...@@ -155,7 +175,7 @@ export default {
</template> </template>
<template #minutes> <template #minutes>
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ rule.elapsedTimeMinutes }} {{ $options.i18n.minutes }} &nbsp;{{ rule.elapsedTimeMinutes }} {{ $options.i18n.minutes }}
</span> </span>
</template> </template>
<template #then> <template #then>
...@@ -165,12 +185,17 @@ export default { ...@@ -165,12 +185,17 @@ export default {
<gl-icon name="notifications" class="gl-mr-3" /> <gl-icon name="notifications" class="gl-mr-3" />
</template> </template>
<template #doAction> <template #doAction>
{{ $options.ACTIONS[$options.DEFAULT_ACTION].toLowerCase() }} {{ getActionName(rule) }}
&nbsp;
</template> </template>
<template #schedule> <template #scheduleOrUser>
<span class="gl-font-weight-bold"> <span v-if="hasEscalationSchedule(rule)" class="gl-font-weight-bold">
{{ rule.oncallSchedule.name }} {{ rule.oncallSchedule.name }}
</span> </span>
<gl-token v-else-if="hasEscalationUser(rule)" view-only>
<gl-avatar :src="rule.user.avatarUrl" :size="16" />
{{ rule.user.name }}
</gl-token>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </div>
......
...@@ -11,13 +11,14 @@ import { ...@@ -11,13 +11,14 @@ import {
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { ACTIONS, ALERT_STATUSES } from '../constants'; import { ACTIONS, ALERT_STATUSES, EMAIL_ONCALL_SCHEDULE_USER, EMAIL_USER } from '../constants';
import UserSelect from './user_select.vue';
export const i18n = { export const i18n = {
fields: { fields: {
rules: { rules: {
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} %{scheduleOrUser}'),
selectSchedule: s__('EscalationPolicies|Select schedule'), selectSchedule: s__('EscalationPolicies|Select schedule'),
noSchedules: s__( noSchedules: s__(
'EscalationPolicies|A schedule is required for adding an escalation policy. Please create an on-call schedule first.', 'EscalationPolicies|A schedule is required for adding an escalation policy. Please create an on-call schedule first.',
...@@ -27,6 +28,9 @@ export const i18n = { ...@@ -27,6 +28,9 @@ export const i18n = {
'EscalationPolicies|A schedule is required for adding an escalation policy.', 'EscalationPolicies|A schedule is required for adding an escalation policy.',
), ),
invalidTimeValidationMsg: s__('EscalationPolicies|Minutes must be between 0 and 1440.'), invalidTimeValidationMsg: s__('EscalationPolicies|Minutes must be between 0 and 1440.'),
invalidUserValidationMsg: s__(
'EscalationPolicies|A user is required for adding an escalation policy.',
),
}, },
}, },
}; };
...@@ -35,6 +39,8 @@ export default { ...@@ -35,6 +39,8 @@ export default {
i18n, i18n,
ALERT_STATUSES, ALERT_STATUSES,
ACTIONS, ACTIONS,
EMAIL_ONCALL_SCHEDULE_USER,
EMAIL_USER,
components: { components: {
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
...@@ -44,6 +50,7 @@ export default { ...@@ -44,6 +50,7 @@ export default {
GlButton, GlButton,
GlIcon, GlIcon,
GlSprintf, GlSprintf,
UserSelect,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -74,12 +81,15 @@ export default { ...@@ -74,12 +81,15 @@ export default {
}, },
}, },
data() { data() {
const { status, elapsedTimeMinutes, action, oncallScheduleIid } = this.rule; const { status, elapsedTimeMinutes, oncallScheduleIid, username, action } = this.rule;
return { return {
status, status,
elapsedTimeMinutes,
action, action,
elapsedTimeMinutes,
oncallScheduleIid, oncallScheduleIid,
username,
hasFocus: true,
}; };
}, },
computed: { computed: {
...@@ -92,7 +102,7 @@ export default { ...@@ -92,7 +102,7 @@ export default {
return !this.schedulesLoading && !this.schedules.length; return !this.schedulesLoading && !this.schedules.length;
}, },
isValid() { isValid() {
return this.isTimeValid && this.isScheduleValid; return this.isTimeValid && this.isScheduleValid && this.isUserValid;
}, },
isTimeValid() { isTimeValid() {
return this.validationState?.isTimeValid; return this.validationState?.isTimeValid;
...@@ -100,21 +110,71 @@ export default { ...@@ -100,21 +110,71 @@ export default {
isScheduleValid() { isScheduleValid() {
return this.validationState?.isScheduleValid; return this.validationState?.isScheduleValid;
}, },
isUserValid() {
return this.validationState?.isUserValid;
},
isEmailOncallScheduleUserActionSelected() {
return this.action === EMAIL_ONCALL_SCHEDULE_USER;
},
isEmailUserActionSelected() {
return this.action === EMAIL_USER;
},
actionBasedRequestParams() {
if (this.isEmailOncallScheduleUserActionSelected) {
return { oncallScheduleIid: parseInt(this.oncallScheduleIid, 10) };
}
return { username: this.username };
},
showEmptyScheduleValidationMsg() {
return this.isEmailOncallScheduleUserActionSelected && !this.isScheduleValid;
},
showNoUserValidationMsg() {
return this.isEmailUserActionSelected && !this.isUserValid;
},
},
mounted() {
this.ruleContainer = this.$refs.ruleContainer?.$el;
this.ruleContainer?.addEventListener('focusin', this.addFocus);
this.ruleContainer?.addEventListener('focusout', this.removeFocus);
},
beforeDestroy() {
this.ruleContainer?.removeEventListener('focusin', this.addFocus);
this.ruleContainer?.removeEventListener('focusout', this.removeFocus);
}, },
methods: { methods: {
addFocus() {
this.hasFocus = true;
},
removeFocus() {
this.hasFocus = false;
},
setOncallSchedule({ iid }) { setOncallSchedule({ iid }) {
this.oncallScheduleIid = this.oncallScheduleIid === iid ? null : iid; this.oncallScheduleIid = this.oncallScheduleIid === iid ? null : iid;
this.emitUpdate(); this.emitUpdate();
}, },
setAction(action) {
this.action = action;
if (this.isEmailOncallScheduleUserActionSelected) {
this.username = null;
} else if (this.isEmailUserActionSelected) {
this.oncallScheduleIid = null;
}
this.emitUpdate();
},
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;
this.emitUpdate(); this.emitUpdate();
}, },
setSelectedUser(username) {
this.username = username;
this.emitUpdate();
},
emitUpdate() { emitUpdate() {
this.$emit('update-escalation-rule', { this.$emit('update-escalation-rule', {
index: this.index, index: this.index,
rule: { rule: {
oncallScheduleIid: parseInt(this.oncallScheduleIid, 10), ...this.actionBasedRequestParams,
action: this.action, action: this.action,
status: this.status, status: this.status,
elapsedTimeMinutes: this.elapsedTimeMinutes, elapsedTimeMinutes: this.elapsedTimeMinutes,
...@@ -126,7 +186,7 @@ export default { ...@@ -126,7 +186,7 @@ export default {
</script> </script>
<template> <template>
<gl-card class="gl-border-gray-400 gl-bg-gray-10 gl-mb-3 gl-relative"> <gl-card ref="ruleContainer" class="gl-border-gray-400 gl-bg-gray-10 gl-mb-3 gl-relative">
<gl-button <gl-button
v-if="index !== 0" v-if="index !== 0"
category="tertiary" category="tertiary"
...@@ -138,10 +198,13 @@ export default { ...@@ -138,10 +198,13 @@ export default {
/> />
<gl-form-group :state="isValid" class="gl-mb-0"> <gl-form-group :state="isValid" class="gl-mb-0">
<template #invalid-feedback> <template #invalid-feedback>
<div v-if="!isScheduleValid"> <div v-if="!isScheduleValid && !hasFocus">
{{ $options.i18n.fields.rules.emptyScheduleValidationMsg }} {{ $options.i18n.fields.rules.emptyScheduleValidationMsg }}
</div> </div>
<div v-if="!isTimeValid" class="gl-display-inline-block gl-mt-2"> <div v-if="!isUserValid && !hasFocus" class="gl-display-inline-block gl-mt-2">
{{ $options.i18n.fields.rules.invalidUserValidationMsg }}
</div>
<div v-if="!isTimeValid && !hasFocus" class="gl-display-inline-block gl-mt-2">
{{ $options.i18n.fields.rules.invalidTimeValidationMsg }} {{ $options.i18n.fields.rules.invalidTimeValidationMsg }}
</div> </div>
</template> </template>
...@@ -181,20 +244,22 @@ export default { ...@@ -181,20 +244,22 @@ export default {
<template #doAction> <template #doAction>
<gl-dropdown <gl-dropdown
class="rule-control gl-mx-3" class="rule-control gl-mx-3"
:text="$options.ACTIONS[rule.action]" :text="$options.ACTIONS[action]"
data-testid="action-dropdown" data-testid="action-dropdown"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="(label, ruleAction) in $options.ACTIONS" v-for="(label, ruleAction) in $options.ACTIONS"
:key="ruleAction" :key="ruleAction"
:is-checked="rule.action === ruleAction" :is-checked="action === ruleAction"
is-check-item is-check-item
@click="setAction(ruleAction)"
> >
{{ label }} {{ label }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
<template #schedule> <template #scheduleOrUser>
<template v-if="isEmailOncallScheduleUserActionSelected">
<gl-dropdown <gl-dropdown
:disabled="noSchedules" :disabled="noSchedules"
class="rule-control" class="rule-control"
...@@ -225,6 +290,8 @@ export default { ...@@ -225,6 +290,8 @@ export default {
data-testid="no-schedules-info-icon" data-testid="no-schedules-info-icon"
/> />
</template> </template>
<user-select v-else :selected-user-name="username" @select-user="setSelectedUser" />
</template>
</gl-sprintf> </gl-sprintf>
</div> </div>
</gl-form-group> </gl-form-group>
......
<script>
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlToken } from '@gitlab/ui';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
import { s__, __ } from '~/locale';
export default {
components: {
GlTokenSelector,
GlAvatar,
GlAvatarLabeled,
GlToken,
},
inject: ['projectPath'],
i18n: {
placeholder: s__('EscalationPolicies|Search for user'),
noResults: __('No matching results'),
},
props: {
selectedUserName: {
type: String,
required: false,
default: null,
},
},
apollo: {
users: {
query: searchProjectMembersQuery,
variables() {
return {
fullPath: this.projectPath,
search: this.search,
};
},
update({ project: { projectMembers: { nodes = [] } = {} } = {} } = {}) {
return nodes.filter((x) => x?.user).map(({ user }) => ({ ...user }));
},
error(error) {
this.error = error;
},
result() {
this.setSelectedUser();
},
debounce: 250,
},
},
data() {
return {
users: [],
selectedUsers: [],
search: '',
};
},
computed: {
loading() {
return this.$apollo.queries.users.loading;
},
placeholderText() {
return this.selectedUsers.length ? '' : this.$options.i18n.placeholder;
},
user() {
return this.selectedUsers[0];
},
},
methods: {
filterUsers(searchTerm) {
this.search = searchTerm;
},
emitUserUpdate() {
this.$emit('select-user', this.user?.username);
},
clearSelectedUsers() {
this.selectedUsers = [];
this.emitUserUpdate();
},
setSelectedUser() {
const selectedUser = this.users.find(({ username }) => username === this.selectedUserName);
if (selectedUser) {
this.selectedUsers.push(selectedUser);
}
},
},
};
</script>
<template>
<div
v-if="selectedUsers.length"
class="gl-inset-border-1-gray-400 gl-px-3 gl-py-2 gl-rounded-base rule-control"
>
<gl-token @close="clearSelectedUsers">
<gl-avatar :src="user.avatarUrl" :size="16" />
{{ user.name }}
</gl-token>
</div>
<gl-token-selector
v-else
ref="tokenSelector"
v-model="selectedUsers"
:dropdown-items="users"
:loading="loading"
:placeholder="placeholderText"
container-class="rule-control"
@text-input="filterUsers"
@token-add="emitUserUpdate"
>
<template #dropdown-item-content="{ dropdownItem }">
<gl-avatar-labeled
:src="dropdownItem.avatarUrl"
:size="32"
:label="dropdownItem.name"
:sub-label="dropdownItem.username"
/>
</template>
</gl-token-selector>
</template>
...@@ -5,10 +5,12 @@ export const ALERT_STATUSES = { ...@@ -5,10 +5,12 @@ export const ALERT_STATUSES = {
RESOLVED: s__('AlertManagement|Resolved'), RESOLVED: s__('AlertManagement|Resolved'),
}; };
export const DEFAULT_ACTION = 'EMAIL_ONCALL_SCHEDULE_USER'; export const EMAIL_ONCALL_SCHEDULE_USER = 'EMAIL_ONCALL_SCHEDULE_USER';
export const EMAIL_USER = 'EMAIL_USER';
export const ACTIONS = { export const ACTIONS = {
[DEFAULT_ACTION]: s__('EscalationPolicies|Email on-call user in schedule'), [EMAIL_ONCALL_SCHEDULE_USER]: s__('EscalationPolicies|Email on-call user in schedule'),
[EMAIL_USER]: s__('EscalationPolicies|Email user'),
}; };
export const DEFAULT_ESCALATION_RULE = { export const DEFAULT_ESCALATION_RULE = {
......
...@@ -10,5 +10,10 @@ fragment EscalationPolicy on EscalationPolicyType { ...@@ -10,5 +10,10 @@ fragment EscalationPolicy on EscalationPolicyType {
iid iid
name name
} }
user {
username
name
avatarUrl
}
} }
} }
import { pickBy, isNull, isNaN } from 'lodash';
import { EMAIL_ONCALL_SCHEDULE_USER, EMAIL_USER } from './constants';
/** /**
* Returns `true` for non-empty string, otherwise returns `false` * Returns `true` for non-empty string, otherwise returns `false`
* @param {String} name * @param {String} name
...@@ -15,11 +18,12 @@ export const isNameFieldValid = (name) => { ...@@ -15,11 +18,12 @@ export const isNameFieldValid = (name) => {
* @returns {Array} * @returns {Array}
*/ */
export const getRulesValidationState = (rules) => { export const getRulesValidationState = (rules) => {
return rules.map((rule) => { return rules.map(({ elapsedTimeMinutes, oncallScheduleIid, username, action }) => {
const minutes = parseInt(rule.elapsedTimeMinutes, 10); const minutes = parseInt(elapsedTimeMinutes, 10);
return { return {
isTimeValid: minutes >= 0 && minutes <= 1440, isTimeValid: minutes >= 0 && minutes <= 1440,
isScheduleValid: Boolean(rule.oncallScheduleIid), isScheduleValid: action === EMAIL_ONCALL_SCHEDULE_USER ? Boolean(oncallScheduleIid) : true,
isUserValid: action === EMAIL_USER ? Boolean(username) : true,
}; };
}); });
}; };
...@@ -30,10 +34,14 @@ export const getRulesValidationState = (rules) => { ...@@ -30,10 +34,14 @@ export const getRulesValidationState = (rules) => {
* *
* @returns {Object} rule * @returns {Object} rule
*/ */
export const serializeRule = ({ elapsedTimeMinutes, ...ruleParams }) => ({ export const serializeRule = ({ elapsedTimeMinutes, ...ruleParams }) => {
...ruleParams, const params = { ...ruleParams };
delete params.action;
return {
...params,
elapsedTimeSeconds: elapsedTimeMinutes * 60, elapsedTimeSeconds: elapsedTimeMinutes * 60,
}); };
};
/** /**
* Parses a policy by converting elapsed seconds to minutes * Parses a policy by converting elapsed seconds to minutes
...@@ -48,3 +56,29 @@ export const parsePolicy = (policy) => ({ ...@@ -48,3 +56,29 @@ export const parsePolicy = (policy) => ({
elapsedTimeMinutes: elapsedTimeSeconds / 60, elapsedTimeMinutes: elapsedTimeSeconds / 60,
})), })),
}); });
/**
* Parses a rule for the UI form usage or doe BE params serializing
* @param {Array} of transformed rules from BE
*
* @returns {Array} of rules
*/
export const getRules = (rules) => {
return rules.map(
({ status, elapsedTimeMinutes, oncallScheduleIid, oncallSchedule, user, username }) => {
const actionBasedProps = pickBy(
{
username: username ?? user?.username,
oncallScheduleIid: parseInt(oncallScheduleIid ?? oncallSchedule?.iid, 10),
},
(prop) => !(isNull(prop) || isNaN(prop)),
);
return {
status,
elapsedTimeMinutes,
...actionBasedProps,
};
},
);
};
...@@ -24,7 +24,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -24,7 +24,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5" class="gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5"
> >
<div <div
class="gl-mb-5" class="gl-display-flex gl-align-items-center gl-mb-5"
> >
<gl-icon-stub <gl-icon-stub
class="gl-mr-3" class="gl-mr-3"
...@@ -38,7 +38,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -38,7 +38,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
class="gl-font-weight-bold" class="gl-font-weight-bold"
> >
1 mins  1 mins
</span> </span>
...@@ -57,6 +57,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -57,6 +57,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
/> />
THEN THEN
email on-call user in schedule email on-call user in schedule
 
<span <span
class="gl-font-weight-bold" class="gl-font-weight-bold"
...@@ -67,7 +68,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -67,7 +68,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
</span> </span>
</div> </div>
<div <div
class="" class="gl-display-flex gl-align-items-center"
> >
<gl-icon-stub <gl-icon-stub
class="gl-mr-3" class="gl-mr-3"
...@@ -81,7 +82,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -81,7 +82,7 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
class="gl-font-weight-bold" class="gl-font-weight-bold"
> >
2 mins  2 mins
</span> </span>
...@@ -99,15 +100,25 @@ exports[`EscalationPolicy renders a policy with rules 1`] = ` ...@@ -99,15 +100,25 @@ exports[`EscalationPolicy renders a policy with rules 1`] = `
size="16" size="16"
/> />
THEN THEN
email on-call user in schedule email user
 
<span <gl-token-stub
class="gl-font-weight-bold" variant="default"
viewonly="true"
> >
<gl-avatar-stub
alt="avatar"
entityid="0"
entityname=""
shape="circle"
size="16"
src="avatar.com/lena.png"
/>
Monitor schedule Lena
</span> </gl-token-stub>
</div> </div>
</div> </div>
</gl-collapse-stub> </gl-collapse-stub>
......
...@@ -96,16 +96,28 @@ describe('AddEscalationPolicyForm', () => { ...@@ -96,16 +96,28 @@ describe('AddEscalationPolicyForm', () => {
expect(wrapper.emitted('update-escalation-policy-form')).toBeUndefined(); expect(wrapper.emitted('update-escalation-policy-form')).toBeUndefined();
}); });
it('on rule update emitted should update rules array and emit updates up', () => { it('on rule update emitted should update rules array and emit updates up', async () => {
const ruleBeforeUpdate = {
status: 'RESOLVED',
elapsedTimeMinutes: 3,
username: 'user',
};
createComponent({ props: { form: { rules: [ruleBeforeUpdate] } } });
await wrapper.vm.$nextTick();
const updatedRule = { const updatedRule = {
status: 'TRIGGERED', status: 'TRIGGERED',
elapsedTimeMinutes: 3, elapsedTimeMinutes: 3,
oncallScheduleIid: 2, oncallScheduleIid: 2,
}; };
findRules().at(0).vm.$emit('update-escalation-rule', { index: 0, rule: updatedRule }); findRules().at(0).vm.$emit('update-escalation-rule', { index: 0, rule: updatedRule });
expect(wrapper.emitted('update-escalation-policy-form')[0]).toEqual([ const emittedValue = wrapper.emitted('update-escalation-policy-form')[0];
expect(emittedValue).toEqual([
{ field: 'rules', value: [expect.objectContaining(updatedRule)] }, { field: 'rules', value: [expect.objectContaining(updatedRule)] },
]); ]);
expect(emittedValue).not.toEqual([
{ field: 'rules', value: [expect.objectContaining(ruleBeforeUpdate)] },
]);
}); });
it('on rule removal emitted should update rules array and emit updates up', () => { it('on rule removal emitted should update rules array and emit updates up', () => {
......
...@@ -9,6 +9,7 @@ import AddEscalationPolicyModal, { ...@@ -9,6 +9,7 @@ import AddEscalationPolicyModal, {
import { import {
addEscalationPolicyModalId, addEscalationPolicyModalId,
editEscalationPolicyModalId, editEscalationPolicyModalId,
EMAIL_ONCALL_SCHEDULE_USER,
} from 'ee/escalation_policies/constants'; } from 'ee/escalation_policies/constants';
import createEscalationPolicyMutation from 'ee/escalation_policies/graphql/mutations/create_escalation_policy.mutation.graphql'; import createEscalationPolicyMutation from 'ee/escalation_policies/graphql/mutations/create_escalation_policy.mutation.graphql';
import updateEscalationPolicyMutation from 'ee/escalation_policies/graphql/mutations/update_escalation_policy.mutation.graphql'; import updateEscalationPolicyMutation from 'ee/escalation_policies/graphql/mutations/update_escalation_policy.mutation.graphql';
...@@ -267,7 +268,14 @@ describe('AddEditsEscalationPolicyModal', () => { ...@@ -267,7 +268,14 @@ describe('AddEditsEscalationPolicyModal', () => {
}); });
form.vm.$emit('update-escalation-policy-form', { form.vm.$emit('update-escalation-policy-form', {
field: 'rules', field: 'rules',
value: [{ status: 'RESOLVED', elapsedTimeMinutes: 1, oncallScheduleIid: 1 }], value: [
{
status: 'RESOLVED',
elapsedTimeMinutes: 1,
action: EMAIL_ONCALL_SCHEDULE_USER,
oncallScheduleIid: 1,
},
],
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toContainEqual({ disabled: false }); expect(findModal().props('actionPrimary').attributes).toContainEqual({ disabled: false });
......
import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui'; import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import EscalationRule, { i18n } from 'ee/escalation_policies/components/escalation_rule.vue'; import EscalationRule, { i18n } from 'ee/escalation_policies/components/escalation_rule.vue';
import { DEFAULT_ESCALATION_RULE, ACTIONS, ALERT_STATUSES } from 'ee/escalation_policies/constants'; import UserSelect from 'ee/escalation_policies/components/user_select.vue';
import {
DEFAULT_ESCALATION_RULE,
ACTIONS,
ALERT_STATUSES,
EMAIL_ONCALL_SCHEDULE_USER,
EMAIL_USER,
} from 'ee/escalation_policies/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const mockSchedules = [ const mockSchedules = [
...@@ -11,6 +18,7 @@ const mockSchedules = [ ...@@ -11,6 +18,7 @@ const mockSchedules = [
]; ];
const emptyScheduleMsg = i18n.fields.rules.emptyScheduleValidationMsg; const emptyScheduleMsg = i18n.fields.rules.emptyScheduleValidationMsg;
const noUserSelecteddErrorMsg = i18n.fields.rules.invalidUserValidationMsg;
const invalidTimeMsg = i18n.fields.rules.invalidTimeValidationMsg; const invalidTimeMsg = i18n.fields.rules.invalidTimeValidationMsg;
describe('EscalationRule', () => { describe('EscalationRule', () => {
...@@ -48,7 +56,7 @@ describe('EscalationRule', () => { ...@@ -48,7 +56,7 @@ 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 findUserSelect = () => wrapper.findComponent(UserSelect);
const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findNoSchedulesInfoIcon = () => wrapper.findByTestId('no-schedules-info-icon'); const findNoSchedulesInfoIcon = () => wrapper.findByTestId('no-schedules-info-icon');
...@@ -94,25 +102,67 @@ describe('EscalationRule', () => { ...@@ -94,25 +102,67 @@ describe('EscalationRule', () => {
expect(findSchedulesDropdown().attributes('disabled')).toBe('true'); expect(findSchedulesDropdown().attributes('disabled')).toBe('true');
expect(findNoSchedulesInfoIcon().exists()).toBe(true); expect(findNoSchedulesInfoIcon().exists()).toBe(true);
}); });
it('should not render UserSelect when action is EMAIL_ONCALL_SCHEDULE_USER', () => {
createComponent({
props: {
rule: {
...DEFAULT_ESCALATION_RULE,
action: EMAIL_ONCALL_SCHEDULE_USER,
},
},
});
expect(findUserSelect().exists()).toBe(false);
});
});
describe('User select', () => {
beforeEach(() => {
createComponent({
props: {
rule: {
...DEFAULT_ESCALATION_RULE,
action: EMAIL_USER,
},
},
});
});
it('should render UserSelect when action is EMAIL USER', () => {
expect(findUserSelect().exists()).toBe(true);
});
it('should NOT render schedule selection dropdown when action is EMAIL USER', () => {
expect(findSchedulesDropdown().exists()).toBe(false);
});
}); });
describe('Validation', () => { describe('Validation', () => {
describe.each` describe.each`
validationState | formState validationState | formState | action
${{ isTimeValid: true, isScheduleValid: true }} | ${'true'} ${{ isTimeValid: true, isScheduleValid: true, isUserValid: true }} | ${'true'} | ${EMAIL_ONCALL_SCHEDULE_USER}
${{ isTimeValid: false, isScheduleValid: true }} | ${undefined} ${{ isTimeValid: false, isScheduleValid: true, isUserValid: true }} | ${undefined} | ${EMAIL_ONCALL_SCHEDULE_USER}
${{ isTimeValid: true, isScheduleValid: false }} | ${undefined} ${{ isTimeValid: true, isScheduleValid: false, isUserValid: true }} | ${undefined} | ${EMAIL_ONCALL_SCHEDULE_USER}
${{ isTimeValid: false, isScheduleValid: false }} | ${undefined} ${{ isTimeValid: true, isScheduleValid: true, isUserValid: false }} | ${undefined} | ${EMAIL_USER}
`(`when`, ({ validationState, formState }) => { ${{ isTimeValid: false, isScheduleValid: false, isUserValid: true }} | ${undefined} | ${EMAIL_ONCALL_SCHEDULE_USER}
${{ isTimeValid: false, isScheduleValid: true, isUserValid: false }} | ${undefined} | ${EMAIL_USER}
`(`when`, ({ validationState, formState, action }) => {
describe(`elapsed minutes control is ${ describe(`elapsed minutes control is ${
validationState.isTimeValid ? 'valid' : 'invalid' validationState.isTimeValid ? 'valid' : 'invalid'
} and schedule control is ${validationState.isScheduleValid ? 'valid' : 'invalid'}`, () => { } and schedule control is ${
validationState.isScheduleValid ? 'valid' : 'invalid'
} and user control is ${validationState.isUserValid ? 'valid' : 'invalid'}`, () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
props: { props: {
validationState, validationState,
rule: {
...DEFAULT_ESCALATION_RULE,
action,
},
}, },
}); });
wrapper.setData({ hasFocus: false });
}); });
it(`sets form group validation state to ${formState}`, () => { it(`sets form group validation state to ${formState}`, () => {
...@@ -123,17 +173,26 @@ describe('EscalationRule', () => { ...@@ -123,17 +173,26 @@ describe('EscalationRule', () => {
validationState.isTimeValid ? 'not show' : 'show' validationState.isTimeValid ? 'not show' : 'show'
} invalid time error message && does ${ } invalid time error message && does ${
validationState.isScheduleValid ? 'not show' : 'show' validationState.isScheduleValid ? 'not show' : 'show'
} invalid schedule error message `, () => { } no schedule error message && does ${
validationState.isUserValid ? 'not show' : 'show'
} no user error message `, () => {
if (validationState.isTimeValid) { if (validationState.isTimeValid) {
expect(findFormGroup().text()).not.toContain(invalidTimeMsg); expect(findFormGroup().text()).not.toContain(invalidTimeMsg);
} else { } else {
expect(findFormGroup().text()).toContain(invalidTimeMsg); expect(findFormGroup().text()).toContain(invalidTimeMsg);
} }
if (validationState.isScheduleValid) { if (validationState.isScheduleValid) {
expect(findFormGroup().text()).not.toContain(emptyScheduleMsg); expect(findFormGroup().text()).not.toContain(emptyScheduleMsg);
} else { } else {
expect(findFormGroup().text()).toContain(emptyScheduleMsg); expect(findFormGroup().text()).toContain(emptyScheduleMsg);
} }
if (validationState.isUserValid) {
expect(findFormGroup().text()).not.toContain(noUserSelecteddErrorMsg);
} else {
expect(findFormGroup().text()).toContain(noUserSelecteddErrorMsg);
}
}); });
}); });
}); });
......
...@@ -18,6 +18,7 @@ export const getEscalationPoliciesQueryResponse = { ...@@ -18,6 +18,7 @@ export const getEscalationPoliciesQueryResponse = {
name: 'Schedule', name: 'Schedule',
__typename: 'IncidentManagementOncallSchedule', __typename: 'IncidentManagementOncallSchedule',
}, },
user: null,
__typename: 'EscalationRuleType', __typename: 'EscalationRuleType',
}, },
], ],
......
...@@ -17,9 +17,10 @@ ...@@ -17,9 +17,10 @@
"id": "gid://gitlab/IncidentManagement::EscalationRule/23", "id": "gid://gitlab/IncidentManagement::EscalationRule/23",
"status": "RESOLVED", "status": "RESOLVED",
"elapsedTimeSeconds": 120, "elapsedTimeSeconds": 120,
"oncallSchedule": { "user": {
"iid": "4", "username": "sharlatenok",
"name": "Monitor schedule" "name": "Lena",
"avatarUrl": "avatar.com/lena.png"
} }
} }
] ]
......
import { GlTokenSelector, GlAvatar, GlToken } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserSelect from 'ee/escalation_policies/components/user_select.vue';
const mockUsers = [
{ id: 1, name: 'User 1', avatarUrl: 'avatar.com/user1.png' },
{ id: 2, name: 'User2', avatarUrl: 'avatar.com/user1.png' },
];
describe('UserSelect', () => {
let wrapper;
const projectPath = 'group/project';
const createComponent = () => {
wrapper = shallowMount(UserSelect, {
data() {
return {
users: mockUsers,
};
},
mocks: {
$apollo: {
queries: {
users: { loading: false },
},
},
},
stubs: {
GlTokenSelector,
},
provide: {
projectPath,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findSelectedUserToken = () => wrapper.findComponent(GlToken);
const findAvatar = () => wrapper.findComponent(GlAvatar);
describe('When no user selected', () => {
it('renders token selector and provides it with correct params', () => {
const tokenSelector = findTokenSelector();
expect(tokenSelector.exists()).toBe(true);
expect(tokenSelector.props('dropdownItems')).toEqual(mockUsers);
expect(tokenSelector.props('loading')).toEqual(false);
});
it('does not render selected user token', () => {
expect(findSelectedUserToken().exists()).toBe(false);
});
});
describe('On user selected', () => {
it('hides token selector', async () => {
const tokenSelector = findTokenSelector();
expect(tokenSelector.exists()).toBe(true);
tokenSelector.vm.$emit('input', [mockUsers[0]]);
await wrapper.vm.$nextTick();
expect(tokenSelector.exists()).toBe(false);
});
it('shows selected user token with name and avatar', async () => {
const selectedUser = mockUsers[0];
findTokenSelector().vm.$emit('input', [selectedUser]);
await wrapper.vm.$nextTick();
const userToken = findSelectedUserToken();
expect(userToken.exists()).toBe(true);
expect(userToken.text()).toMatchInterpolatedText(selectedUser.name);
const avatar = findAvatar();
expect(avatar.exists()).toBe(true);
expect(avatar.props('src')).toBe(selectedUser.avatarUrl);
});
});
describe('On user deselected', () => {
it('hides selected user token and avatar, shows token selector', async () => {
// select user
findTokenSelector().vm.$emit('input', [mockUsers[0]]);
await wrapper.vm.$nextTick();
const userToken = findSelectedUserToken();
expect(userToken.exists()).toBe(true);
// deselect user
userToken.vm.$emit('close');
await wrapper.vm.$nextTick();
expect(userToken.exists()).toBe(false);
expect(findTokenSelector().exists()).toBe(true);
});
});
});
import { EMAIL_ONCALL_SCHEDULE_USER, EMAIL_USER } from 'ee/escalation_policies/constants';
import * as utils from 'ee/escalation_policies/utils';
describe('Escalation policies utility functions', () => {
describe('isNameFieldValid', () => {
it('should return `true` when name is valid', () => {
expect(utils.isNameFieldValid('policy name')).toBe(true);
});
it('should return `false` otherwise', () => {
expect(utils.isNameFieldValid('')).toBe(false);
expect(utils.isNameFieldValid(undefined)).toBe(false);
});
});
describe('getRulesValidationState', () => {
it.each`
rules | validationState
${[{ elapsedTimeMinutes: 10, oncallScheduleIid: 1, username: null, action: EMAIL_ONCALL_SCHEDULE_USER }]} | ${[{ isTimeValid: true, isScheduleValid: true, isUserValid: true }]}
${[{ elapsedTimeMinutes: 1500, oncallScheduleIid: 1, username: null, action: EMAIL_ONCALL_SCHEDULE_USER }]} | ${[{ isTimeValid: false, isScheduleValid: true, isUserValid: true }]}
${[{ elapsedTimeMinutes: -2, oncallScheduleIid: null, username: 'user', action: EMAIL_ONCALL_SCHEDULE_USER }]} | ${[{ isTimeValid: false, isScheduleValid: false, isUserValid: true }]}
${[{ elapsedTimeMinutes: 30, oncallScheduleIid: null, username: 'user', action: EMAIL_USER }]} | ${[{ isTimeValid: true, isScheduleValid: true, isUserValid: true }]}
${[{ elapsedTimeMinutes: 30, oncallScheduleIid: 1, username: null, action: EMAIL_USER }]} | ${[{ isTimeValid: true, isScheduleValid: true, isUserValid: false }]}
`('calculates rules validation state', ({ rules, validationState }) => {
expect(utils.getRulesValidationState(rules)).toEqual(validationState);
});
});
describe('parsePolicy', () => {
it('parses a policy by converting elapsed seconds to minutes for ecach rule', () => {
const policy = {
name: 'policy',
rules: [
{ elapsedTimeSeconds: 600, username: 'user' },
{ elapsedTimeSeconds: 0, oncallScheduleIid: 1 },
],
};
expect(utils.parsePolicy(policy)).toEqual({
name: 'policy',
rules: [
{ elapsedTimeMinutes: 10, username: 'user' },
{ elapsedTimeMinutes: 0, oncallScheduleIid: 1 },
],
});
});
});
describe('getRules', () => {
it.each`
rules | transformedRules
${[{ elapsedTimeMinutes: 10, status: 'Acknowledged', oncallScheduleIid: '1', username: null }]} | ${[{ elapsedTimeMinutes: 10, status: 'Acknowledged', oncallScheduleIid: 1 }]}
${[{ elapsedTimeMinutes: 20, status: 'Resolved', oncallSchedule: { iid: '2' }, username: null }]} | ${[{ elapsedTimeMinutes: 20, status: 'Resolved', oncallScheduleIid: 2 }]}
${[{ elapsedTimeMinutes: 0, status: 'Resolved', oncallScheduleId: null, username: 'user' }]} | ${[{ elapsedTimeMinutes: 0, status: 'Resolved', username: 'user' }]}
${[{ elapsedTimeMinutes: 40, status: 'Resolved', oncallScheduleId: null, user: { username: 'user2' } }]} | ${[{ elapsedTimeMinutes: 40, status: 'Resolved', username: 'user2' }]}
`('transforms the rules', ({ rules, transformedRules }) => {
expect(utils.getRules(rules)).toEqual(transformedRules);
});
});
});
...@@ -13139,6 +13139,9 @@ msgstr "" ...@@ -13139,6 +13139,9 @@ msgstr ""
msgid "EscalationPolicies|A schedule is required for adding an escalation policy. Please create an on-call schedule first." msgid "EscalationPolicies|A schedule is required for adding an escalation policy. Please create an on-call schedule first."
msgstr "" msgstr ""
msgid "EscalationPolicies|A user is required for adding an escalation policy."
msgstr ""
msgid "EscalationPolicies|Add an escalation policy" msgid "EscalationPolicies|Add an escalation policy"
msgstr "" msgstr ""
...@@ -13163,6 +13166,9 @@ msgstr "" ...@@ -13163,6 +13166,9 @@ msgstr ""
msgid "EscalationPolicies|Email on-call user in schedule" msgid "EscalationPolicies|Email on-call user in schedule"
msgstr "" msgstr ""
msgid "EscalationPolicies|Email user"
msgstr ""
msgid "EscalationPolicies|Escalation policies" msgid "EscalationPolicies|Escalation policies"
msgstr "" msgstr ""
...@@ -13172,7 +13178,7 @@ msgstr "" ...@@ -13172,7 +13178,7 @@ msgstr ""
msgid "EscalationPolicies|Failed to load oncall-schedules" msgid "EscalationPolicies|Failed to load oncall-schedules"
msgstr "" msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{schedule}" msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{scheduleOrUser}"
msgstr "" msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes" msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes"
...@@ -13187,13 +13193,16 @@ msgstr "" ...@@ -13187,13 +13193,16 @@ msgstr ""
msgid "EscalationPolicies|Remove escalation rule" msgid "EscalationPolicies|Remove escalation rule"
msgstr "" msgstr ""
msgid "EscalationPolicies|Search for user"
msgstr ""
msgid "EscalationPolicies|Select schedule" msgid "EscalationPolicies|Select schedule"
msgstr "" msgstr ""
msgid "EscalationPolicies|Set up escalation policies to define who is paged, and when, in the event the first users paged don't respond." msgid "EscalationPolicies|Set up escalation policies to define who is paged, and when, in the event the first users paged don't respond."
msgstr "" msgstr ""
msgid "EscalationPolicies|THEN %{doAction} %{schedule}" msgid "EscalationPolicies|THEN %{doAction} %{scheduleOrUser}"
msgstr "" msgstr ""
msgid "EscalationPolicies|The escalation policy could not be deleted. Please try again." msgid "EscalationPolicies|The escalation policy could not be deleted. Please try again."
......
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