Commit e4d3899d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '268366-display-escalation-policy' into 'master'

Display Escalation Policy

See merge request gitlab-org/gitlab!63416
parents 85e509c2 c58680e2
@import 'mixins_and_variables_and_functions';
.escalation-policy-modal {
width: 640px;
}
......@@ -9,3 +11,27 @@
.rule-close-icon {
right: 1rem;
}
$stroke-size: 1px;
.right-arrow {
@include gl-relative;
@include gl-mx-5;
@include gl-display-inline-block;
@include gl-vertical-align-middle;
height: $stroke-size;
background-color: var(--gray-900, $gray-900);
min-width: $gl-spacing-scale-7;
&-head {
@include gl-absolute;
top: -2*$stroke-size;
left: calc(100% - #{5*$stroke-size});
@include gl-display-inline-block;
@include gl-p-1;
@include gl-border-solid;
border-width: 0 $stroke-size $stroke-size 0;
border-color: var(--gray-900, $gray-900);
transform: rotate(-45deg);
}
}
......@@ -3,7 +3,7 @@ import { GlLink, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { defaultEscalationRule } from '../constants';
import { DEFAULT_ESCALATION_RULE } from '../constants';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import EscalationRule from './escalation_rule.vue';
......@@ -74,11 +74,11 @@ export default {
},
},
mounted() {
this.rules.push({ ...cloneDeep(defaultEscalationRule), key: this.getUid() });
this.addRule();
},
methods: {
addRule() {
this.rules.push({ ...cloneDeep(defaultEscalationRule), key: this.getUid() });
this.rules.push({ ...cloneDeep(DEFAULT_ESCALATION_RULE), key: this.getUid() });
},
updateEscalationRules(index, rule) {
this.rules[index] = { ...this.rules[index], ...rule };
......
......@@ -3,7 +3,9 @@ import { GlModal, GlAlert } from '@gitlab/ui';
import { set } from 'lodash';
import { s__, __ } from '~/locale';
import { addEscalationPolicyModalId } from '../constants';
import { updateStoreOnEscalationPolicyCreate } from '../graphql/cache_updates';
import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import { isNameFieldValid, getRulesValidationState } from '../utils';
import AddEditEscalationPolicyForm from './add_edit_escalation_policy_form.vue';
......@@ -86,6 +88,11 @@ export default {
...this.getRequestParams(),
},
},
update(store, { data }) {
updateStoreOnEscalationPolicyCreate(store, getEscalationPoliciesQuery, data, {
projectPath,
});
},
})
.then(
({
......
<script>
import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import { GlEmptyState, GlButton, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { addEscalationPolicyModalId } from '../constants';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import AddEscalationPolicyModal from './add_edit_escalation_policy_modal.vue';
import EscalationPolicy from './escalation_policy.vue';
export const i18n = {
title: s__('EscalationPolicies|Escalation policies'),
addPolicy: s__('EscalationPolicies|Add policy'),
emptyState: {
title: s__('EscalationPolicies|Create an escalation policy in GitLab'),
description: s__(
......@@ -20,18 +25,73 @@ export default {
components: {
GlEmptyState,
GlButton,
GlLoadingIcon,
AddEscalationPolicyModal,
EscalationPolicy,
},
directives: {
GlModal: GlModalDirective,
},
inject: ['emptyEscalationPoliciesSvgPath'],
inject: ['projectPath', 'emptyEscalationPoliciesSvgPath'],
data() {
return {
escalationPolicies: [],
};
},
apollo: {
escalationPolicies: {
query: getEscalationPoliciesQuery,
variables() {
return {
projectPath: this.projectPath,
};
},
update({ project }) {
return project?.incidentManagementEscalationPolicies?.nodes ?? [];
},
error(error) {
Sentry.captureException(error);
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.escalationPolicies.loading;
},
hasPolicies() {
return this.escalationPolicies.length;
},
},
};
</script>
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<template v-else-if="hasPolicies">
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<h2>{{ $options.i18n.title }}</h2>
<gl-button
v-gl-modal="$options.addEscalationPolicyModalId"
:title="$options.i18n.addPolicy"
category="secondary"
variant="confirm"
class="gl-mt-5"
>
{{ $options.i18n.addPolicy }}
</gl-button>
</div>
<escalation-policy
v-for="(policy, index) in escalationPolicies"
:key="policy.id"
:policy="policy"
:index="index"
/>
</template>
<gl-empty-state
v-else
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:svg-path="emptyEscalationPoliciesSvgPath"
......@@ -42,6 +102,6 @@ export default {
</gl-button>
</template>
</gl-empty-state>
<add-escalation-policy-modal />
<add-escalation-policy-modal :modal-id="$options.addEscalationPolicyModalId" />
</div>
</template>
<script>
import {
GlModalDirective,
GlTooltipDirective,
GlButton,
GlButtonGroup,
GlCard,
GlSprintf,
GlIcon,
GlCollapse,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { ACTIONS, ALERT_STATUSES, DEFAULT_ACTION } from '../constants';
export const i18n = {
editPolicy: s__('EscalationPolicies|Edit escalation policy'),
deletePolicy: s__('EscalationPolicies|Delete escalation policy'),
escalationRule: s__(
'EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{schedule}',
),
minutes: s__('EscalationPolicies|mins'),
};
const isRuleValid = ({ status, elapsedTimeSeconds, oncallSchedule: { name } }) =>
Object.keys(ALERT_STATUSES).includes(status) &&
typeof elapsedTimeSeconds === 'number' &&
typeof name === 'string';
export default {
i18n,
ACTIONS,
ALERT_STATUSES,
DEFAULT_ACTION,
components: {
GlButton,
GlButtonGroup,
GlCard,
GlSprintf,
GlIcon,
GlCollapse,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
props: {
policy: {
type: Object,
required: true,
validator: ({ name, rules }) => {
return typeof name === 'string' && Array.isArray(rules) && rules.every(isRuleValid);
},
},
index: {
type: Number,
required: true,
},
},
data() {
return {
isPolicyVisible: this.index === 0,
};
},
computed: {
policyVisibleAngleIcon() {
return this.isPolicyVisible ? 'angle-down' : 'angle-right';
},
policyVisibleAngleIconLabel() {
return this.isPolicyVisible ? __('Collapse') : __('Expand');
},
},
};
</script>
<template>
<gl-card
class="gl-mt-5"
:class="{ 'gl-border-bottom-0': !isPolicyVisible }"
:body-class="{ 'gl-p-0': !isPolicyVisible }"
:header-class="{ 'gl-py-3': true, 'gl-rounded-base': !isPolicyVisible }"
>
<template #header>
<div class="gl-display-flex gl-align-items-center">
<gl-button
v-gl-tooltip
class="gl-mr-2 gl-p-0!"
:title="policyVisibleAngleIconLabel"
:aria-label="policyVisibleAngleIconLabel"
category="tertiary"
@click="isPolicyVisible = !isPolicyVisible"
>
<gl-icon :size="12" :name="policyVisibleAngleIcon" />
</gl-button>
<h3 class="gl-font-weight-bold gl-font-lg gl-m-0">{{ policy.name }}</h3>
<gl-button-group class="gl-ml-auto">
<gl-button
v-gl-tooltip
:title="$options.i18n.editPolicy"
icon="pencil"
:aria-label="$options.i18n.editPolicy"
disabled
/>
<gl-button
v-gl-tooltip
:title="$options.i18n.deletePolicy"
icon="remove"
:aria-label="$options.i18n.deletePolicy"
disabled
/>
</gl-button-group>
</div>
</template>
<gl-collapse :visible="isPolicyVisible">
<p v-if="policy.description" class="gl-text-gray-500 gl-mb-5">
{{ policy.description }}
</p>
<div class="gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5">
<div
v-for="(rule, ruleIndex) in policy.rules"
:key="rule.id"
:class="{ 'gl-mb-5': ruleIndex !== policy.rules.length - 1 }"
>
<gl-icon name="clock" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.escalationRule">
<template #alertStatus>
{{ $options.ALERT_STATUSES[rule.status].toLowerCase() }}
</template>
<template #minutes>
<span class="gl-font-weight-bold">
{{ rule.elapsedTimeSeconds }} {{ $options.i18n.minutes }}
</span>
</template>
<template #then>
<span class="right-arrow">
<i class="right-arrow-head"></i>
</span>
<gl-icon name="notifications" class="gl-mr-3" />
</template>
<template #doAction>
{{ $options.ACTIONS[$options.DEFAULT_ACTION].toLowerCase() }}
</template>
<template #schedule>
<span class="gl-font-weight-bold">
{{ rule.oncallSchedule.name }}
</span>
</template>
</gl-sprintf>
</div>
</div>
</gl-collapse>
</gl-card>
</template>
......@@ -5,11 +5,13 @@ export const ALERT_STATUSES = {
RESOLVED: s__('AlertManagement|Resolved'),
};
export const DEFAULT_ACTION = 'EMAIL_ONCALL_SCHEDULE_USER';
export const ACTIONS = {
EMAIL_ONCALL_SCHEDULE_USER: s__('EscalationPolicies|Email on-call user in schedule'),
[DEFAULT_ACTION]: s__('EscalationPolicies|Email on-call user in schedule'),
};
export const defaultEscalationRule = {
export const DEFAULT_ESCALATION_RULE = {
status: 'ACKNOWLEDGED',
elapsedTimeSeconds: 0,
action: 'EMAIL_ONCALL_SCHEDULE_USER',
......@@ -17,3 +19,4 @@ export const defaultEscalationRule = {
};
export const addEscalationPolicyModalId = 'addEscalationPolicyModal';
export const editEscalationPolicyModalId = 'editEscalationPolicyModal';
import produce from 'immer';
const addEscalationPolicyToStore = (store, query, { escalationPolicyCreate }, variables) => {
const policy = escalationPolicyCreate?.escalationPolicy;
if (!policy) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
draftData.project.incidentManagementEscalationPolicies.nodes.push(policy);
});
store.writeQuery({
query,
variables,
data,
});
};
export const hasErrors = ({ errors = [] }) => errors?.length;
export const updateStoreOnEscalationPolicyCreate = (store, query, data, variables) => {
if (!hasErrors(data)) {
addEscalationPolicyToStore(store, query, data, variables);
}
};
fragment EscalationPolicy on EscalationPolicyType {
id
name
description
rules {
id
status
elapsedTimeSeconds
oncallSchedule {
iid
name
}
}
}
#import "../fragments/escalation_policy.fragment.graphql"
mutation escalationPolicyCreate($input: EscalationPolicyCreateInput!) {
escalationPolicyCreate(input: $input) {
escalationPolicy {
id
name
description
rules {
status
elapsedTimeSeconds
oncallSchedule {
iid
name
}
}
...EscalationPolicy
}
errors
}
......
#import "../fragments/escalation_policy.fragment.graphql"
query getEscalationPolicies($projectPath: ID!) {
project(fullPath: $projectPath) {
incidentManagementEscalationPolicies {
nodes {
...EscalationPolicy
}
}
}
}
......@@ -19,6 +19,7 @@ const apolloProvider = new VueApollo({
return defaultDataIdFromObject(object);
},
},
assumeImmutableResults: true,
},
),
});
......
......@@ -237,7 +237,7 @@ export default {
class="gl-mt-5"
:class="{ 'gl-border-bottom-0': !scheduleVisible }"
:body-class="{ 'gl-p-0': !scheduleVisible }"
header-class="gl-py-3"
:header-class="{ 'gl-py-3': true, 'gl-rounded-small': !scheduleVisible }"
>
<template #header>
<div
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EscalationPolicy renders policy with rules 1`] = `
<gl-card-stub
bodyclass="[object Object]"
class="gl-mt-5"
footerclass=""
headerclass="[object Object]"
>
<gl-collapse-stub
visible="true"
>
<p
class="gl-text-gray-500 gl-mb-5"
>
Description 1 lives here
</p>
<div
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-p-5"
>
<div
class="gl-mb-5"
>
<gl-icon-stub
class="gl-mr-3"
name="clock"
size="16"
/>
IF alert is not
acknowledged
in
<span
class="gl-font-weight-bold"
>
10 mins
</span>
<span
class="right-arrow"
>
<i
class="right-arrow-head"
/>
</span>
<gl-icon-stub
class="gl-mr-3"
name="notifications"
size="16"
/>
THEN
email on-call user in schedule
<span
class="gl-font-weight-bold"
>
Schedule to fill in
</span>
</div>
<div
class=""
>
<gl-icon-stub
class="gl-mr-3"
name="clock"
size="16"
/>
IF alert is not
resolved
in
<span
class="gl-font-weight-bold"
>
20 mins
</span>
<span
class="right-arrow"
>
<i
class="right-arrow-head"
/>
</span>
<gl-icon-stub
class="gl-mr-3"
name="notifications"
size="16"
/>
THEN
email on-call user in schedule
<span
class="gl-font-weight-bold"
>
Monitor schedule
</span>
</div>
</div>
</gl-collapse-stub>
</gl-card-stub>
`;
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AddEscalationPolicyForm, {
i18n,
} from 'ee/escalation_policies/components/add_edit_escalation_policy_form.vue';
import EscalationRule from 'ee/escalation_policies/components/escalation_rule.vue';
import { defaultEscalationRule } from 'ee/escalation_policies/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockPolicy from './mocks/mockPolicy.json';
import { DEFAULT_ESCALATION_RULE } from 'ee/escalation_policies/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import mockPolicies from './mocks/mockPolicies.json';
describe('AddEscalationPolicyForm', () => {
let wrapper;
const projectPath = 'group/project';
const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(AddEscalationPolicyForm, {
propsData: {
form: {
name: mockPolicy.name,
description: mockPolicy.description,
},
validationState: {
name: true,
rules: [],
},
...props,
wrapper = shallowMountExtended(AddEscalationPolicyForm, {
propsData: {
form: {
name: mockPolicies[1].name,
description: mockPolicies[1].description,
},
provide: {
projectPath,
validationState: {
name: true,
rules: [],
},
mocks: {
$apollo: {
queries: { schedules: { loading: false } },
},
...props,
},
provide: {
projectPath,
},
mocks: {
$apollo: {
queries: { schedules: { loading: false } },
},
}),
);
},
});
};
beforeEach(() => {
......@@ -65,7 +63,7 @@ describe('AddEscalationPolicyForm', () => {
await wrapper.vm.$nextTick();
const rules = findRules();
expect(rules.length).toBe(2);
expect(rules.at(1).props('rule')).toMatchObject(defaultEscalationRule);
expect(rules.at(1).props('rule')).toMatchObject(DEFAULT_ESCALATION_RULE);
});
it('should NOT emit updates when rule is added', async () => {
......
import { GlModal, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import AddEscalationPolicyForm from 'ee/escalation_policies/components/add_edit_escalation_policy_form.vue';
import AddEscalationPolicyModal, {
i18n,
} from 'ee/escalation_policies/components/add_edit_escalation_policy_modal.vue';
import waitForPromises from 'helpers/wait_for_promises';
import mockPolicy from './mocks/mockPolicy.json';
import mockPolicies from './mocks/mockPolicies.json';
describe('AddEscalationPolicyModal', () => {
let wrapper;
const projectPath = 'group/project';
const mockHideModal = jest.fn();
const mutate = jest.fn();
const mockPolicy = cloneDeep(mockPolicies[0]);
const createComponent = ({ escalationPolicy, data } = {}) => {
wrapper = shallowMount(AddEscalationPolicyModal, {
......@@ -60,14 +62,23 @@ describe('AddEscalationPolicyModal', () => {
it('makes a request with form data to create an escalation policy', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
const rules = mockPolicy.rules.map(
({ status, elapsedTimeSeconds, oncallSchedule: { id } }) => ({
status,
elapsedTimeSeconds,
oncallScheduleIid: id,
}),
);
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
projectPath,
...mockPolicy,
rules,
},
},
update: expect.any(Function),
}),
);
});
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import EscalationPolicy from 'ee/escalation_policies/components/escalation_policy.vue';
import mockPolicies from './mocks/mockPolicies.json';
describe('EscalationPolicy', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(EscalationPolicy, {
propsData: {
policy: cloneDeep(mockPolicies[0]),
index: 0,
},
stubs: {
GlSprintf,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders policy with rules', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EscalationPoliciesWrapper, {
i18n,
} from 'ee/escalation_policies/components/escalation_policies_wrapper.vue';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import EscalationPoliciesWrapper from 'ee/escalation_policies/components/escalation_policies_wrapper.vue';
import EscalationPolicy from 'ee/escalation_policies/components/escalation_policy.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import mockEscalationPolicies from './mocks/mockPolicies.json';
describe('AlertManagementEmptyState', () => {
describe('Escalation Policies Wrapper', () => {
let wrapper;
const emptyEscalationPoliciesSvgPath = 'illustration/path.svg';
const projectPath = 'group/project';
function mountComponent() {
wrapper = shallowMount(EscalationPoliciesWrapper, {
function mountComponent({ loading = false, escalationPolicies = [] } = {}) {
const $apollo = {
queries: {
escalationPolicies: {
loading,
},
},
};
wrapper = shallowMountExtended(EscalationPoliciesWrapper, {
provide: {
emptyEscalationPoliciesSvgPath,
projectPath,
},
mocks: {
$apollo,
},
data() {
return {
escalationPolicies,
};
},
});
}
......@@ -24,15 +41,40 @@ describe('AlertManagementEmptyState', () => {
wrapper.destroy();
});
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findEscalationPolicies = () => wrapper.findAllComponents(EscalationPolicy);
const findAddPolicyBtn = () =>
wrapper.findByRole('button', { name: EscalationPoliciesWrapper.i18n.addPolicy });
describe.each`
state | loading | escalationPolicies | showsEmptyState | showsLoader
${'is loading'} | ${true} | ${[]} | ${false} | ${true}
${'is empty'} | ${false} | ${[]} | ${true} | ${false}
${'has policies'} | ${false} | ${mockEscalationPolicies} | ${false} | ${false}
`(``, ({ state, loading, escalationPolicies, showsEmptyState, showsLoader }) => {
describe(`When ${state}`, () => {
beforeEach(() => {
mountComponent({
loading,
escalationPolicies,
});
});
it(`does ${loading ? 'show' : 'not show'} a loader`, () => {
expect(findLoader().exists()).toBe(showsLoader);
});
it(`does ${showsEmptyState ? 'show' : 'not show'} an empty state`, () => {
expect(findEmptyState().exists()).toBe(showsEmptyState);
});
it(`does ${escalationPolicies.length ? 'show' : 'not show'} escalation policies`, () => {
expect(findEscalationPolicies()).toHaveLength(escalationPolicies.length);
});
describe('Empty state', () => {
it('shows empty state and passed correct attributes to it', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().attributes()).toEqual({
title: i18n.emptyState.title,
description: i18n.emptyState.description,
svgpath: emptyEscalationPoliciesSvgPath,
it(`does ${escalationPolicies.length ? 'show' : 'not show'} "Add policy" button`, () => {
expect(findAddPolicyBtn().exists()).toBe(Boolean(escalationPolicies.length));
});
});
});
......
import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import EscalationRule, { i18n } from 'ee/escalation_policies/components/escalation_rule.vue';
import { defaultEscalationRule, ACTIONS, ALERT_STATUSES } from 'ee/escalation_policies/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { DEFAULT_ESCALATION_RULE, ACTIONS, ALERT_STATUSES } from 'ee/escalation_policies/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const mockSchedules = [
{ id: 1, name: 'schedule1' },
......@@ -17,22 +16,20 @@ const invalidTimeMsg = i18n.fields.rules.invalidTimeValidationMsg;
describe('EscalationRule', () => {
let wrapper;
const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(EscalationRule, {
propsData: {
rule: cloneDeep(defaultEscalationRule),
schedules: mockSchedules,
schedulesLoading: false,
index: 0,
isValid: false,
...props,
},
stubs: {
GlFormGroup,
GlSprintf,
},
}),
);
wrapper = shallowMountExtended(EscalationRule, {
propsData: {
rule: cloneDeep(DEFAULT_ESCALATION_RULE),
schedules: mockSchedules,
schedulesLoading: false,
index: 0,
isValid: false,
...props,
},
stubs: {
GlFormGroup,
GlSprintf,
},
});
};
beforeEach(() => {
......
[
{
"id": "37",
"name": "Escalation policy",
"description": "Description 1 lives here",
"rules": [
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/22",
"status": "ACKNOWLEDGED",
"elapsedTimeSeconds": 10,
"oncallSchedule": {
"iid": "3",
"name": "Schedule to fill in"
}
},
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/23",
"status": "RESOLVED",
"elapsedTimeSeconds": 20,
"oncallSchedule": {
"iid": "4",
"name": "Monitor schedule"
}
}
]
},
{
"id": "39",
"name": "Another escalation policy",
"description": "Description 1 lives here",
"rules": [
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/48",
"status": "ACKNOWLEDGED",
"elapsedTimeSeconds": 30,
"oncallSchedule": {
"iid": "3",
"name": "Schedule to fill in"
}
}
]
}
]
{
"iid": "37",
"name": "Test ecsaltion policy",
"description": "Description 1 lives here",
"rules": []
}
......@@ -13063,9 +13063,15 @@ msgstr ""
msgid "EscalationPolicies|Add escalation policy"
msgstr ""
msgid "EscalationPolicies|Add policy"
msgstr ""
msgid "EscalationPolicies|Create an escalation policy in GitLab"
msgstr ""
msgid "EscalationPolicies|Delete escalation policy"
msgstr ""
msgid "EscalationPolicies|Edit escalation policy"
msgstr ""
......@@ -13075,12 +13081,18 @@ msgstr ""
msgid "EscalationPolicies|Email on-call user in schedule"
msgstr ""
msgid "EscalationPolicies|Escalation policies"
msgstr ""
msgid "EscalationPolicies|Escalation rules"
msgstr ""
msgid "EscalationPolicies|Failed to load oncall-schedules"
msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{schedule}"
msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes"
msgstr ""
......@@ -13096,6 +13108,9 @@ msgstr ""
msgid "EscalationPolicies|THEN %{doAction} %{schedule}"
msgstr ""
msgid "EscalationPolicies|mins"
msgstr ""
msgid "Estimate"
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