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
6ff310b5
Commit
6ff310b5
authored
Oct 15, 2020
by
Tristan Read
Committed by
Olena Horal-Koretska
Oct 15, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[RUN AS-IF-FOSS] Add SLA to incident list
parent
6c20d183
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
258 additions
and
38 deletions
+258
-38
app/assets/javascripts/incidents/components/incidents_list.vue
...ssets/javascripts/incidents/components/incidents_list.vue
+33
-19
app/assets/javascripts/incidents/constants.js
app/assets/javascripts/incidents/constants.js
+1
-0
app/assets/javascripts/incidents/list.js
app/assets/javascripts/incidents/list.js
+4
-1
ee/app/assets/javascripts/incidents/components/service_level_agreement_cell.vue
...pts/incidents/components/service_level_agreement_cell.vue
+51
-0
ee/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
...idents/graphql/fragments/incident_fields.fragment.graphql
+1
-0
ee/app/helpers/ee/projects/incidents_helper.rb
ee/app/helpers/ee/projects/incidents_helper.rb
+4
-5
ee/spec/frontend/incidents/components/service_level_agreement_cell_spec.js
...incidents/components/service_level_agreement_cell_spec.js
+72
-0
ee/spec/helpers/ee/projects/incidents_helper_spec.rb
ee/spec/helpers/ee/projects/incidents_helper_spec.rb
+38
-8
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/incidents/components/incidents_list_spec.js
spec/frontend/incidents/components/incidents_list_spec.js
+40
-2
spec/frontend/incidents/mocks/incidents.json
spec/frontend/incidents/mocks/incidents.json
+4
-2
spec/helpers/projects/incidents_helper_spec.rb
spec/helpers/projects/incidents_helper_spec.rb
+1
-1
No files found.
app/assets/javascripts/incidents/components/incidents_list.vue
View file @
6ff310b5
...
...
@@ -39,6 +39,7 @@ import {
DEFAULT_PAGE_SIZE
,
INCIDENT_STATUS_TABS
,
TH_CREATED_AT_TEST_ID
,
TH_INCIDENT_SLA_TEST_ID
,
TH_SEVERITY_TEST_ID
,
TH_PUBLISHED_TEST_ID
,
INCIDENT_DETAILS_PATH
,
...
...
@@ -67,7 +68,7 @@ export default {
{
key
:
'
severity
'
,
label
:
s__
(
'
IncidentManagement|Severity
'
),
thClass
,
thClass
:
`
${
thClass
}
w-15p`
,
tdClass
:
`
${
tdClass
}
sortable-cell`
,
sortable
:
true
,
thAttr
:
TH_SEVERITY_TEST_ID
,
...
...
@@ -75,23 +76,38 @@ export default {
{
key
:
'
title
'
,
label
:
s__
(
'
IncidentManagement|Incident
'
),
thClass
:
`gl-pointer-events-none
gl-w-half
`
,
thClass
:
`gl-pointer-events-none`
,
tdClass
,
},
{
key
:
'
createdAt
'
,
label
:
s__
(
'
IncidentManagement|Date created
'
),
thClass
,
thClass
:
`
${
thClass
}
gl-w-eighth`
,
tdClass
:
`
${
tdClass
}
sortable-cell`
,
sortable
:
true
,
thAttr
:
TH_CREATED_AT_TEST_ID
,
},
{
key
:
'
incidentSla
'
,
label
:
s__
(
'
IncidentManagement|Time to SLA
'
),
thClass
:
`gl-pointer-events-none gl-text-right gl-w-eighth`
,
tdClass
:
`
${
tdClass
}
gl-text-right`
,
thAttr
:
TH_INCIDENT_SLA_TEST_ID
,
},
{
key
:
'
assignees
'
,
label
:
s__
(
'
IncidentManagement|Assignees
'
),
thClass
:
'
gl-pointer-events-none
'
,
thClass
:
'
gl-pointer-events-none
w-15p
'
,
tdClass
,
},
{
key
:
'
published
'
,
label
:
s__
(
'
IncidentManagement|Published
'
),
thClass
:
`
${
thClass
}
w-15p`
,
tdClass
:
`
${
tdClass
}
sortable-cell`
,
sortable
:
true
,
thAttr
:
TH_PUBLISHED_TEST_ID
,
},
],
components
:
{
GlLoadingIcon
,
...
...
@@ -107,6 +123,8 @@ export default {
GlTabs
,
GlTab
,
PublishedCell
:
()
=>
import
(
'
ee_component/incidents/components/published_cell.vue
'
),
ServiceLevelAgreementCell
:
()
=>
import
(
'
ee_component/incidents/components/service_level_agreement_cell.vue
'
),
GlBadge
,
GlEmptyState
,
SeverityToken
,
...
...
@@ -126,6 +144,7 @@ export default {
'
textQuery
'
,
'
authorUsernamesQuery
'
,
'
assigneeUsernamesQuery
'
,
'
slaFeatureAvailable
'
,
],
apollo
:
{
incidents
:
{
...
...
@@ -231,21 +250,12 @@ export default {
);
},
availableFields
()
{
return
this
.
publishedAvailable
?
[
...
this
.
$options
.
fields
,
...[
{
key
:
'
published
'
,
label
:
s__
(
'
IncidentManagement|Published
'
),
thClass
,
tdClass
:
`
${
tdClass
}
sortable-cell`
,
sortable
:
true
,
thAttr
:
TH_PUBLISHED_TEST_ID
,
},
],
]
:
this
.
$options
.
fields
;
const
isHidden
=
{
published
:
!
this
.
publishedAvailable
,
incidentSla
:
!
this
.
slaFeatureAvailable
,
};
return
this
.
$options
.
fields
.
filter
(({
key
})
=>
!
isHidden
[
key
]);
},
isEmpty
()
{
return
!
this
.
incidents
.
list
?.
length
;
...
...
@@ -526,6 +536,10 @@ export default {
<time-ago-tooltip
:time=
"item.createdAt"
/>
</
template
>
<
template
v-if=
"slaFeatureAvailable"
#cell(incidentSla)=
"{ item }"
>
<service-level-agreement-cell
:sla-due-at=
"item.slaDueAt"
data-testid=
"incident-sla"
/>
</
template
>
<
template
#cell(assignees)=
"{ item }"
>
<div
data-testid=
"incident-assignees"
>
<template
v-if=
"hasAssignees(item.assignees)"
>
...
...
app/assets/javascripts/incidents/constants.js
View file @
6ff310b5
...
...
@@ -46,5 +46,6 @@ export const trackIncidentCreateNewOptions = {
export
const
DEFAULT_PAGE_SIZE
=
20
;
export
const
TH_CREATED_AT_TEST_ID
=
{
'
data-testid
'
:
'
incident-management-created-at-sort
'
};
export
const
TH_SEVERITY_TEST_ID
=
{
'
data-testid
'
:
'
incident-management-severity-sort
'
};
export
const
TH_INCIDENT_SLA_TEST_ID
=
{
'
data-testid
'
:
'
incident-management-sla
'
};
export
const
TH_PUBLISHED_TEST_ID
=
{
'
data-testid
'
:
'
incident-management-published-sort
'
};
export
const
INCIDENT_DETAILS_PATH
=
'
incident
'
;
app/assets/javascripts/incidents/list.js
View file @
6ff310b5
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
import
IncidentsList
from
'
./components/incidents_list.vue
'
;
Vue
.
use
(
VueApollo
);
...
...
@@ -19,6 +20,7 @@ export default () => {
textQuery
,
authorUsernamesQuery
,
assigneeUsernamesQuery
,
slaFeatureAvailable
,
}
=
domEl
.
dataset
;
const
apolloProvider
=
new
VueApollo
({
...
...
@@ -33,11 +35,12 @@ export default () => {
incidentType
,
newIssuePath
,
issuePath
,
publishedAvailable
,
publishedAvailable
:
parseBoolean
(
publishedAvailable
)
,
emptyListSvgPath
,
textQuery
,
authorUsernamesQuery
,
assigneeUsernamesQuery
,
slaFeatureAvailable
:
parseBoolean
(
slaFeatureAvailable
),
},
apolloProvider
,
components
:
{
...
...
ee/app/assets/javascripts/incidents/components/service_level_agreement_cell.vue
0 → 100644
View file @
6ff310b5
<
script
>
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
formatTime
,
calculateRemainingMilliseconds
}
from
'
~/lib/utils/datetime_utility
'
;
export
default
{
i18n
:
{
longText
:
s__
(
'
IncidentManagement|%{hours} hours, %{minutes} minutes remaining
'
),
shortText
:
s__
(
'
IncidentManagement|%{minutes} minutes remaining
'
),
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
slaDueAt
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
},
computed
:
{
shouldShow
()
{
// Checks for a valid date string
return
this
.
slaDueAt
&&
!
Number
.
isNaN
(
Date
.
parse
(
this
.
slaDueAt
));
},
remainingTime
()
{
return
calculateRemainingMilliseconds
(
this
.
slaDueAt
);
},
slaText
()
{
const
remainingDuration
=
formatTime
(
this
.
remainingTime
);
// remove the seconds portion of the string
return
remainingDuration
.
substring
(
0
,
remainingDuration
.
length
-
3
);
},
slaTitle
()
{
const
minutes
=
Math
.
floor
(
this
.
remainingTime
/
1000
/
60
)
%
60
;
const
hours
=
Math
.
floor
(
this
.
remainingTime
/
1000
/
60
/
60
);
if
(
hours
>
0
)
{
return
sprintf
(
this
.
$options
.
i18n
.
longText
,
{
hours
,
minutes
});
}
return
sprintf
(
this
.
$options
.
i18n
.
shortText
,
{
hours
,
minutes
});
},
},
};
</
script
>
<
template
>
<span
v-if=
"shouldShow"
v-gl-tooltip
:title=
"slaTitle"
>
{{
slaText
}}
</span>
</
template
>
ee/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql
View file @
6ff310b5
fragment
IncidentFields
on
Issue
{
severity
statusPagePublishedIncident
slaDueAt
}
ee/app/helpers/ee/projects/incidents_helper.rb
View file @
6ff310b5
...
...
@@ -8,17 +8,16 @@ module EE
override
:incidents_data
def
incidents_data
(
project
,
params
)
super
.
merge
(
incidents_data_
published_availabl
e
(
project
)
incidents_data_
e
e
(
project
)
)
end
private
def
incidents_data_published_available
(
project
)
return
{}
unless
project
.
feature_available?
(
:status_page
)
def
incidents_data_ee
(
project
)
{
'published-available'
=>
'true'
'published-available'
=>
project
.
feature_available?
(
:status_page
).
to_s
,
'sla-feature-available'
=>
::
IncidentManagement
::
IncidentSla
.
available_for?
(
project
).
to_s
}
end
end
...
...
ee/spec/frontend/incidents/components/service_level_agreement_cell_spec.js
0 → 100644
View file @
6ff310b5
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
ServiceLevelAgreementCell
from
'
ee/incidents/components/service_level_agreement_cell.vue
'
;
import
{
calculateRemainingMilliseconds
,
formatTime
}
from
'
~/lib/utils/datetime_utility
'
;
jest
.
mock
(
'
~/lib/utils/datetime_utility
'
,
()
=>
({
calculateRemainingMilliseconds
:
jest
.
fn
(()
=>
1000
),
formatTime
:
jest
.
fn
(()
=>
'
00:00:00
'
),
}));
const
mockDateString
=
'
2020-10-15T02:42:27Z
'
;
describe
(
'
Incidents Published Cell
'
,
()
=>
{
let
wrapper
;
function
mountComponent
(
props
)
{
wrapper
=
shallowMount
(
ServiceLevelAgreementCell
,
{
propsData
:
{
...
props
,
},
});
}
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
});
describe
(
'
Service Level Agreement Cell
'
,
()
=>
{
it
(
'
renders an empty cell by default
'
,
()
=>
{
mountComponent
();
expect
(
wrapper
.
html
()).
toBe
(
''
);
});
it
(
'
renders a empty cell for an invalid date
'
,
()
=>
{
mountComponent
({
slaDueAt
:
'
dfsgsdfg
'
});
expect
(
wrapper
.
html
()).
toBe
(
''
);
});
it
(
'
displays the correct time when displaying an SLA
'
,
()
=>
{
formatTime
.
mockImplementation
(()
=>
'
12:34:56
'
);
mountComponent
({
slaDueAt
:
mockDateString
});
expect
(
wrapper
.
text
()).
toBe
(
'
12:34
'
);
});
describe
(
'
tooltips
'
,
()
=>
{
const
hoursInMilliseconds
=
60
*
60
*
1000
;
const
minutesInMilliseconds
=
60
*
1000
;
it
.
each
`
hours | minutes | expectedMessage
${
5
}
|
${
7
}
|
${
'
5 hours, 7 minutes remaining
'
}
${
5
}
|
${
0
}
|
${
'
5 hours, 0 minutes remaining
'
}
${
0
}
|
${
7
}
|
${
'
7 minutes remaining
'
}
${
0
}
|
${
0
}
|
${
'
0 minutes remaining
'
}
`
(
'
returns the correct message for: hours: "$hours", hinutes: "$minutes"
'
,
({
hours
,
minutes
,
expectedMessage
})
=>
{
const
testTime
=
hours
*
hoursInMilliseconds
+
minutes
*
minutesInMilliseconds
;
calculateRemainingMilliseconds
.
mockImplementation
(()
=>
testTime
);
mountComponent
({
slaDueAt
:
mockDateString
});
expect
(
wrapper
.
attributes
(
'
title
'
)).
toBe
(
expectedMessage
);
},
);
});
});
});
ee/spec/helpers/ee/projects/incidents_helper_spec.rb
View file @
6ff310b5
...
...
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec
.
describe
Projects
::
IncidentsHelper
do
include
Gitlab
::
Routing
.
url_helpers
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
_with_refind
(
:project
)
{
create
(
:project
)
}
let
(
:project_path
)
{
project
.
full_path
}
let
(
:new_issue_path
)
{
new_project_issue_path
(
project
)
}
let
(
:issue_path
)
{
project_issues_path
(
project
)
}
...
...
@@ -26,6 +26,8 @@ RSpec.describe Projects::IncidentsHelper do
'incident-type'
=>
'incident'
,
'issue-path'
=>
issue_path
,
'empty-list-svg-path'
=>
match_asset_path
(
'/assets/illustrations/incident-empty-state.svg'
),
'published-available'
=>
'false'
,
'sla-feature-available'
=>
'false'
,
'text-query'
:
'search text'
,
'author-usernames-query'
:
'root'
,
'assignee-usernames-query'
:
'max.power'
...
...
@@ -34,20 +36,48 @@ RSpec.describe Projects::IncidentsHelper do
subject
{
helper
.
incidents_data
(
project
,
params
)
}
before
do
allow
(
project
).
to
receive
(
:feature_available?
).
with
(
:status_page
).
and_return
(
status_page_feature_available
)
it
'returns the correct set of data'
do
expect
(
subject
).
to
match
(
expected_incidents_data
)
end
context
'when status page feature is available'
do
let
(
:status_page_feature_available
)
{
true
}
before
do
stub_licensed_features
(
status_page:
true
)
end
it
{
is_expected
.
to
match
(
expected_incidents_data
.
merge
(
'published-available'
=>
'true'
))
}
it
'returns the feature as enabled'
do
expect
(
subject
[
'published-available'
]).
to
eq
(
'true'
)
end
end
context
'when status page issue is not available'
do
let
(
:status_page_feature_available
)
{
false
}
context
'when status page feature is not available'
do
before
do
stub_licensed_features
(
status_page:
false
)
end
it
{
is_expected
.
to
match
(
expected_incidents_data
)
}
it
'returns the feature as disabled'
do
expect
(
subject
[
'published-available'
]).
to
eq
(
'false'
)
end
end
context
'when incident sla feature is available'
do
before
do
stub_licensed_features
(
incident_sla:
true
)
end
it
'returns the feature as enabled'
do
expect
(
subject
[
'sla-feature-available'
]).
to
eq
(
'true'
)
end
end
context
'when incident sla feature is not available'
do
before
do
stub_licensed_features
(
incident_sla:
false
)
end
it
'returns the feature as disabled'
do
expect
(
subject
[
'sla-feature-available'
]).
to
eq
(
'false'
)
end
end
end
end
locale/gitlab.pot
View file @
6ff310b5
...
...
@@ -13734,6 +13734,12 @@ msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "IncidentManagement|%{hours} hours, %{minutes} minutes remaining"
msgstr ""
msgid "IncidentManagement|%{minutes} minutes remaining"
msgstr ""
msgid "IncidentManagement|All"
msgstr ""
...
...
@@ -13794,6 +13800,9 @@ msgstr ""
msgid "IncidentManagement|There was an error displaying the incidents."
msgstr ""
msgid "IncidentManagement|Time to SLA"
msgstr ""
msgid "IncidentManagement|Unassigned"
msgstr ""
...
...
spec/frontend/incidents/components/incidents_list_spec.js
View file @
6ff310b5
...
...
@@ -55,6 +55,7 @@ describe('Incidents List', () => {
const
findLoader
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
findTimeAgo
=
()
=>
wrapper
.
findAll
(
TimeAgoTooltip
);
const
findSearch
=
()
=>
wrapper
.
find
(
FilteredSearchBar
);
const
findIncidentSlaHeader
=
()
=>
wrapper
.
find
(
'
[data-testid="incident-management-sla"]
'
);
const
findAssignees
=
()
=>
wrapper
.
findAll
(
'
[data-testid="incident-assignees"]
'
);
const
findCreateIncidentBtn
=
()
=>
wrapper
.
find
(
'
[data-testid="createIncidentBtn"]
'
);
const
findClosedIcon
=
()
=>
wrapper
.
findAll
(
"
[data-testid='incident-closed']
"
);
...
...
@@ -64,11 +65,16 @@ describe('Incidents List', () => {
const
findStatusTabs
=
()
=>
wrapper
.
find
(
GlTabs
);
const
findEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
findSeverity
=
()
=>
wrapper
.
findAll
(
SeverityToken
);
const
findIncidentSla
=
()
=>
wrapper
.
findAll
(
"
[data-testid='incident-sla']
"
);
function
mountComponent
({
data
=
{
incidents
:
[],
incidentsCount
:
{}
},
loading
=
false
})
{
function
mountComponent
({
data
=
{
},
loading
=
false
,
provide
=
{}
}
=
{
})
{
wrapper
=
mount
(
IncidentsList
,
{
data
()
{
return
data
;
return
{
incidents
:
[],
incidentsCount
:
{},
...
data
,
};
},
mocks
:
{
$apollo
:
{
...
...
@@ -90,11 +96,14 @@ describe('Incidents List', () => {
textQuery
:
''
,
authorUsernamesQuery
:
''
,
assigneeUsernamesQuery
:
''
,
slaFeatureAvailable
:
true
,
...
provide
,
},
stubs
:
{
GlButton
:
true
,
GlAvatar
:
true
,
GlEmptyState
:
true
,
ServiceLevelAgreementCell
:
true
,
},
});
}
...
...
@@ -204,6 +213,35 @@ describe('Incidents List', () => {
joinPaths
(
`/project/issues/incident`
,
mockIncidents
[
0
].
iid
),
);
});
describe
(
'
Incident SLA field
'
,
()
=>
{
it
(
'
displays the column when the feature is available
'
,
()
=>
{
mountComponent
({
data
:
{
incidents
:
{
list
:
mockIncidents
}
},
provide
:
{
slaFeatureAvailable
:
true
},
});
expect
(
findIncidentSlaHeader
().
text
()).
toContain
(
'
Time to SLA
'
);
});
it
(
'
does not display the column when the feature is not available
'
,
()
=>
{
mountComponent
({
data
:
{
incidents
:
{
list
:
mockIncidents
}
},
provide
:
{
slaFeatureAvailable
:
false
},
});
expect
(
findIncidentSlaHeader
().
exists
()).
toBe
(
false
);
});
it
(
'
renders an SLA for each incident
'
,
()
=>
{
mountComponent
({
data
:
{
incidents
:
{
list
:
mockIncidents
}
},
provide
:
{
slaFeatureAvailable
:
true
},
});
expect
(
findIncidentSla
().
length
).
toBe
(
mockIncidents
.
length
);
});
});
});
describe
(
'
Create Incident
'
,
()
=>
{
...
...
spec/frontend/incidents/mocks/incidents.json
View file @
6ff310b5
...
...
@@ -5,7 +5,8 @@
"createdAt"
:
"2020-06-03T15:46:08Z"
,
"assignees"
:
{},
"state"
:
"opened"
,
"severity"
:
"CRITICAL"
"severity"
:
"CRITICAL"
,
"slaDueAt"
:
"2020-06-04T12:46:08Z"
},
{
"iid"
:
"14"
,
...
...
@@ -22,7 +23,8 @@
]
},
"state"
:
"opened"
,
"severity"
:
"HIGH"
"severity"
:
"HIGH"
,
"slaDueAt"
:
null
},
{
"iid"
:
"13"
,
...
...
spec/helpers/projects/incidents_helper_spec.rb
View file @
6ff310b5
...
...
@@ -21,7 +21,7 @@ RSpec.describe Projects::IncidentsHelper do
subject
(
:data
)
{
helper
.
incidents_data
(
project
,
params
)
}
it
'returns frontend configuration'
do
expect
(
data
).
to
match
(
expect
(
data
).
to
include
(
'project-path'
=>
project_path
,
'new-issue-path'
=>
new_issue_path
,
'incident-template-name'
=>
'incident'
,
...
...
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