Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
1354c674
Commit
1354c674
authored
Aug 04, 2021
by
Olena Horal-Koretska
Committed by
Nicolò Maria Mezzopera
Aug 04, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add option to email user in escalation policy
parent
de0c2a54
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
608 additions
and
107 deletions
+608
-107
ee/app/assets/javascripts/escalation_policies/components/add_edit_escalation_policy_form.vue
...n_policies/components/add_edit_escalation_policy_form.vue
+12
-9
ee/app/assets/javascripts/escalation_policies/components/add_edit_escalation_policy_modal.vue
..._policies/components/add_edit_escalation_policy_modal.vue
+5
-13
ee/app/assets/javascripts/escalation_policies/components/escalation_policy.vue
...ipts/escalation_policies/components/escalation_policy.vue
+34
-9
ee/app/assets/javascripts/escalation_policies/components/escalation_rule.vue
...cripts/escalation_policies/components/escalation_rule.vue
+107
-40
ee/app/assets/javascripts/escalation_policies/components/user_select.vue
...avascripts/escalation_policies/components/user_select.vue
+116
-0
ee/app/assets/javascripts/escalation_policies/constants.js
ee/app/assets/javascripts/escalation_policies/constants.js
+4
-2
ee/app/assets/javascripts/escalation_policies/graphql/fragments/escalation_policy.fragment.graphql
...cies/graphql/fragments/escalation_policy.fragment.graphql
+5
-0
ee/app/assets/javascripts/escalation_policies/utils.js
ee/app/assets/javascripts/escalation_policies/utils.js
+41
-7
ee/spec/frontend/escalation_policies/__snapshots__/escalation_policy_spec.js.snap
...ion_policies/__snapshots__/escalation_policy_spec.js.snap
+20
-9
ee/spec/frontend/escalation_policies/add_edit_escalation_policy_form_spec.js
...calation_policies/add_edit_escalation_policy_form_spec.js
+14
-2
ee/spec/frontend/escalation_policies/add_edit_escalation_policy_modal_spec.js
...alation_policies/add_edit_escalation_policy_modal_spec.js
+9
-1
ee/spec/frontend/escalation_policies/escalation_rule_spec.js
ee/spec/frontend/escalation_policies/escalation_rule_spec.js
+69
-10
ee/spec/frontend/escalation_policies/mocks/apollo_mock.js
ee/spec/frontend/escalation_policies/mocks/apollo_mock.js
+1
-0
ee/spec/frontend/escalation_policies/mocks/mockPolicies.json
ee/spec/frontend/escalation_policies/mocks/mockPolicies.json
+4
-3
ee/spec/frontend/escalation_policies/user_select_spec.js
ee/spec/frontend/escalation_policies/user_select_spec.js
+97
-0
ee/spec/frontend/escalation_policies/utils_spec.js
ee/spec/frontend/escalation_policies/utils_spec.js
+59
-0
locale/gitlab.pot
locale/gitlab.pot
+11
-2
No files found.
ee/app/assets/javascripts/escalation_policies/components/add_edit_escalation_policy_form.vue
View file @
1354c674
...
@@ -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
)
{
...
...
ee/app/assets/javascripts/escalation_policies/components/add_edit_escalation_policy_modal.vue
View file @
1354c674
...
@@ -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
);
...
...
ee/app/assets/javascripts/escalation_policies/components/escalation_policy.vue
View file @
1354c674
...
@@ -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} %{schedule
OrUser
}
'
,
),
),
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
}}
{{
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
)
}}
</
template
>
</
template
>
<
template
#schedule
>
<
template
#schedule
OrUser
>
<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>
...
...
ee/app/assets/javascripts/escalation_policies/components/escalation_rule.vue
View file @
1354c674
...
@@ -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} %{schedule
OrUser
}
'
),
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>
...
...
ee/app/assets/javascripts/escalation_policies/components/user_select.vue
0 → 100644
View file @
1354c674
<
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>
ee/app/assets/javascripts/escalation_policies/constants.js
View file @
1354c674
...
@@ -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
=
{
...
...
ee/app/assets/javascripts/escalation_policies/graphql/fragments/escalation_policy.fragment.graphql
View file @
1354c674
...
@@ -10,5 +10,10 @@ fragment EscalationPolicy on EscalationPolicyType {
...
@@ -10,5 +10,10 @@ fragment EscalationPolicy on EscalationPolicyType {
iid
iid
name
name
}
}
user
{
username
name
avatarUrl
}
}
}
}
}
ee/app/assets/javascripts/escalation_policies/utils.js
View file @
1354c674
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
,
};
},
);
};
ee/spec/frontend/escalation_policies/__snapshots__/escalation_policy_spec.js.snap
View file @
1354c674
...
@@ -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>
...
...
ee/spec/frontend/escalation_policies/add_edit_escalation_policy_form_spec.js
View file @
1354c674
...
@@ -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
'
,
()
=>
{
...
...
ee/spec/frontend/escalation_policies/add_edit_escalation_policy_modal_spec.js
View file @
1354c674
...
@@ -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
});
...
...
ee/spec/frontend/escalation_policies/escalation_rule_spec.js
View file @
1354c674
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
);
}
});
});
});
});
});
});
...
...
ee/spec/frontend/escalation_policies/mocks/apollo_mock.js
View file @
1354c674
...
@@ -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
'
,
},
},
],
],
...
...
ee/spec/frontend/escalation_policies/mocks/mockPolicies.json
View file @
1354c674
...
@@ -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"
}
}
}
}
]
]
...
...
ee/spec/frontend/escalation_policies/user_select_spec.js
0 → 100644
View file @
1354c674
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
);
});
});
});
ee/spec/frontend/escalation_policies/utils_spec.js
0 → 100644
View file @
1354c674
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
);
});
});
});
locale/gitlab.pot
View file @
1354c674
...
@@ -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} %{schedule
OrUser
}"
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} %{schedule
OrUser
}"
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."
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment