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
63754789
Commit
63754789
authored
Oct 13, 2020
by
Sean Arnold
Committed by
Peter Leitzen
Oct 13, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add incident SLA
- Add model, table etc
parent
9aa0c3b7
Changes
40
Hide whitespace changes
Inline
Side-by-side
Showing
40 changed files
with
616 additions
and
39 deletions
+616
-39
app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
...scripts/issue_show/components/incidents/highlight_bar.vue
+20
-4
app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
...scripts/issue_show/components/incidents/incident_tabs.vue
+1
-1
app/assets/javascripts/issue_show/incident.js
app/assets/javascripts/issue_show/incident.js
+3
-1
app/presenters/issue_presenter.rb
app/presenters/issue_presenter.rb
+2
-0
changelogs/unreleased/241663-incident-sla-logic.yml
changelogs/unreleased/241663-incident-sla-logic.yml
+5
-0
db/migrate/20201002012659_add_issuable_sla_table.rb
db/migrate/20201002012659_add_issuable_sla_table.rb
+12
-0
db/schema_migrations/20201002012659
db/schema_migrations/20201002012659
+1
-0
db/structure.sql
db/structure.sql
+25
-2
doc/api/graphql/reference/gitlab_schema.graphql
doc/api/graphql/reference/gitlab_schema.graphql
+10
-0
doc/api/graphql/reference/gitlab_schema.json
doc/api/graphql/reference/gitlab_schema.json
+28
-0
doc/api/graphql/reference/index.md
doc/api/graphql/reference/index.md
+2
-0
ee/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_sla_due_at.graphql
...mponents/incidents/graphql/queries/get_sla_due_at.graphql
+7
-0
ee/app/assets/javascripts/issue_show/components/incidents/incident_sla.vue
...ascripts/issue_show/components/incidents/incident_sla.vue
+61
-0
ee/app/controllers/ee/projects/settings/operations_controller.rb
...controllers/ee/projects/settings/operations_controller.rb
+1
-1
ee/app/graphql/ee/resolvers/issues_resolver.rb
ee/app/graphql/ee/resolvers/issues_resolver.rb
+12
-0
ee/app/graphql/ee/types/issue_type.rb
ee/app/graphql/ee/types/issue_type.rb
+4
-3
ee/app/helpers/ee/issuables_helper.rb
ee/app/helpers/ee/issuables_helper.rb
+2
-1
ee/app/helpers/ee/operations_helper.rb
ee/app/helpers/ee/operations_helper.rb
+1
-1
ee/app/models/concerns/ee/issuable.rb
ee/app/models/concerns/ee/issuable.rb
+10
-0
ee/app/models/ee/issue.rb
ee/app/models/ee/issue.rb
+1
-0
ee/app/models/issuable_sla.rb
ee/app/models/issuable_sla.rb
+6
-0
ee/app/presenters/ee/issue_presenter.rb
ee/app/presenters/ee/issue_presenter.rb
+13
-0
ee/app/services/ee/issues/create_service.rb
ee/app/services/ee/issues/create_service.rb
+15
-0
ee/app/services/incident_management/incidents/create_sla_service.rb
...vices/incident_management/incidents/create_sla_service.rb
+46
-0
ee/lib/incident_management/incident_sla.rb
ee/lib/incident_management/incident_sla.rb
+11
-0
ee/spec/factories/issuable_sla.rb
ee/spec/factories/issuable_sla.rb
+8
-0
ee/spec/frontend/issue_show/components/incidents/incident_sla_spec.js
...tend/issue_show/components/incidents/incident_sla_spec.js
+74
-0
ee/spec/graphql/types/issue_type_spec.rb
ee/spec/graphql/types/issue_type_spec.rb
+1
-4
ee/spec/models/ee/incident_management/project_incident_management_setting_spec.rb
...nt_management/project_incident_management_setting_spec.rb
+1
-1
ee/spec/models/issuable_sla_spec.rb
ee/spec/models/issuable_sla_spec.rb
+13
-0
ee/spec/models/issue_spec.rb
ee/spec/models/issue_spec.rb
+28
-0
ee/spec/presenters/ee/issue_presenter_spec.rb
ee/spec/presenters/ee/issue_presenter_spec.rb
+28
-0
ee/spec/services/incident_management/incidents/create_sla_service_spec.rb
.../incident_management/incidents/create_sla_service_spec.rb
+90
-0
lib/gitlab/import_export/project/import_export.yml
lib/gitlab/import_export/project/import_export.yml
+6
-0
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
...end/issue_show/components/incidents/highlight_bar_spec.js
+55
-19
spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
...end/issue_show/components/incidents/incident_tabs_spec.js
+0
-1
spec/lib/gitlab/import_export/all_models.yml
spec/lib/gitlab/import_export/all_models.yml
+3
-0
spec/lib/gitlab/import_export/import_test_coverage_spec.rb
spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+1
-0
spec/lib/gitlab/import_export/safe_model_attributes.yml
spec/lib/gitlab/import_export/safe_model_attributes.yml
+3
-0
No files found.
app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue
View file @
63754789
...
...
@@ -5,6 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
export
default
{
components
:
{
GlLink
,
IncidentSla
:
()
=>
import
(
'
ee_component/issue_show/components/incidents/incident_sla.vue
'
),
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
...
...
@@ -12,36 +13,51 @@ export default {
props
:
{
alert
:
{
type
:
Object
,
required
:
true
,
required
:
false
,
default
:
null
,
},
},
data
()
{
return
{
childHasData
:
false
};
},
computed
:
{
startTime
()
{
return
formatDate
(
this
.
alert
.
startedAt
,
'
yyyy-mm-dd Z
'
);
},
showHighlightBar
()
{
return
this
.
alert
||
this
.
childHasData
;
},
},
methods
:
{
update
(
hasData
)
{
this
.
childHasData
=
hasData
;
},
},
};
</
script
>
<
template
>
<div
v-show=
"showHighlightBar"
class=
"gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div
class=
"gl-p
r-3"
>
<div
v-if=
"alert"
class=
"gl-m
r-3"
>
<span
class=
"gl-font-weight-bold"
>
{{
s__
(
'
HighlightBar|Original alert:
'
)
}}
</span>
<gl-link
v-gl-tooltip
:title=
"alert.title"
:href=
"alert.detailsUrl"
>
#
{{
alert
.
iid
}}
</gl-link>
</div>
<div
class=
"gl-p
r-3"
>
<div
v-if=
"alert"
class=
"gl-m
r-3"
>
<span
class=
"gl-font-weight-bold"
>
{{
s__
(
'
HighlightBar|Alert start time:
'
)
}}
</span>
{{
startTime
}}
</div>
<div>
<div
v-if=
"alert"
class=
"gl-mr-3"
>
<span
class=
"gl-font-weight-bold"
>
{{
s__
(
'
HighlightBar|Alert events:
'
)
}}
</span>
<span>
{{
alert
.
eventCount
}}
</span>
</div>
<incident-sla
@
update=
"update"
/>
</div>
</
template
>
app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
View file @
63754789
...
...
@@ -53,7 +53,7 @@ export default {
<div>
<gl-tabs
content-class=
"gl-reset-line-height"
class=
"gl-mt-n3"
data-testid=
"incident-tabs"
>
<gl-tab
:title=
"s__('Incident|Summary')"
>
<highlight-bar
v-if=
"alert"
:alert=
"alert"
/>
<highlight-bar
:alert=
"alert"
/>
<description-component
v-bind=
"$attrs"
/>
</gl-tab>
<gl-tab
v-if=
"alert"
class=
"alert-management-details"
:title=
"s__('Incident|Alert details')"
>
...
...
app/assets/javascripts/issue_show/incident.js
View file @
63754789
...
...
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
issuableApp
from
'
./components/app.vue
'
;
import
incidentTabs
from
'
./components/incidents/incident_tabs.vue
'
;
import
{
parseBoolean
}
from
'
~/lib/utils/common_utils
'
;
Vue
.
use
(
VueApollo
);
...
...
@@ -11,7 +12,7 @@ export default function initIssuableApp(issuableData = {}) {
defaultClient
:
createDefaultClient
(),
});
const
{
projectNamespace
,
projectPath
,
iid
}
=
issuableData
;
const
{
iid
,
projectNamespace
,
projectPath
,
slaFeatureAvailable
}
=
issuableData
;
return
new
Vue
({
el
:
document
.
getElementById
(
'
js-issuable-app
'
),
...
...
@@ -22,6 +23,7 @@ export default function initIssuableApp(issuableData = {}) {
provide
:
{
fullPath
:
`
${
projectNamespace
}
/
${
projectPath
}
`
,
iid
,
slaFeatureAvailable
:
parseBoolean
(
slaFeatureAvailable
),
},
render
(
createElement
)
{
return
createElement
(
'
issuable-app
'
,
{
...
...
app/presenters/issue_presenter.rb
View file @
63754789
...
...
@@ -11,3 +11,5 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue
.
subscribed?
(
current_user
,
issue
.
project
)
end
end
IssuePresenter
.
prepend_if_ee
(
'EE::IssuePresenter'
)
changelogs/unreleased/241663-incident-sla-logic.yml
0 → 100644
View file @
63754789
---
title
:
Add Issuable Service Level Agreement (SLA) table
merge_request
:
44253
author
:
type
:
added
db/migrate/20201002012659_add_issuable_sla_table.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
class
AddIssuableSlaTable
<
ActiveRecord
::
Migration
[
6.0
]
DOWNTIME
=
false
def
change
create_table
:issuable_slas
do
|
t
|
t
.
references
:issue
,
null:
false
,
index:
{
unique:
true
},
foreign_key:
{
on_delete: :cascade
}
t
.
datetime_with_timezone
:due_at
,
null:
false
end
end
end
db/schema_migrations/20201002012659
0 → 100644
View file @
63754789
8a12c3c4f674d2a36df56a89bfd32e0f3945e73605460bdf2a8b0aa1308f5b19
\ No newline at end of file
db/structure.sql
View file @
63754789
...
...
@@ -12739,6 +12739,21 @@ CREATE SEQUENCE issuable_severities_id_seq
ALTER
SEQUENCE
issuable_severities_id_seq
OWNED
BY
issuable_severities
.
id
;
CREATE
TABLE
issuable_slas
(
id
bigint
NOT
NULL
,
issue_id
bigint
NOT
NULL
,
due_at
timestamp
with
time
zone
NOT
NULL
);
CREATE
SEQUENCE
issuable_slas_id_seq
START
WITH
1
INCREMENT
BY
1
NO
MINVALUE
NO
MAXVALUE
CACHE
1
;
ALTER
SEQUENCE
issuable_slas_id_seq
OWNED
BY
issuable_slas
.
id
;
CREATE
TABLE
issue_assignees
(
user_id
integer
NOT
NULL
,
issue_id
integer
NOT
NULL
...
...
@@ -17561,6 +17576,8 @@ ALTER TABLE ONLY ip_restrictions ALTER COLUMN id SET DEFAULT nextval('ip_restric
ALTER
TABLE
ONLY
issuable_severities
ALTER
COLUMN
id
SET
DEFAULT
nextval
(
'issuable_severities_id_seq'
::
regclass
);
ALTER
TABLE
ONLY
issuable_slas
ALTER
COLUMN
id
SET
DEFAULT
nextval
(
'issuable_slas_id_seq'
::
regclass
);
ALTER
TABLE
ONLY
issue_email_participants
ALTER
COLUMN
id
SET
DEFAULT
nextval
(
'issue_email_participants_id_seq'
::
regclass
);
ALTER
TABLE
ONLY
issue_links
ALTER
COLUMN
id
SET
DEFAULT
nextval
(
'issue_links_id_seq'
::
regclass
);
...
...
@@ -18689,6 +18706,9 @@ ALTER TABLE ONLY ip_restrictions
ALTER
TABLE
ONLY
issuable_severities
ADD
CONSTRAINT
issuable_severities_pkey
PRIMARY
KEY
(
id
);
ALTER
TABLE
ONLY
issuable_slas
ADD
CONSTRAINT
issuable_slas_pkey
PRIMARY
KEY
(
id
);
ALTER
TABLE
ONLY
issue_email_participants
ADD
CONSTRAINT
issue_email_participants_pkey
PRIMARY
KEY
(
id
);
...
...
@@ -20485,6 +20505,8 @@ CREATE INDEX index_ip_restrictions_on_group_id ON ip_restrictions USING btree (g
CREATE
UNIQUE
INDEX
index_issuable_severities_on_issue_id
ON
issuable_severities
USING
btree
(
issue_id
);
CREATE
UNIQUE
INDEX
index_issuable_slas_on_issue_id
ON
issuable_slas
USING
btree
(
issue_id
);
CREATE
UNIQUE
INDEX
index_issue_assignees_on_issue_id_and_user_id
ON
issue_assignees
USING
btree
(
issue_id
,
user_id
);
CREATE
INDEX
index_issue_assignees_on_user_id
ON
issue_assignees
USING
btree
(
user_id
);
...
...
@@ -20541,8 +20563,6 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at);
CREATE
INDEX
index_issues_on_updated_by_id
ON
issues
USING
btree
(
updated_by_id
)
WHERE
(
updated_by_id
IS
NOT
NULL
);
CREATE
INDEX
index_issues_project_id_issue_type_incident
ON
issues
USING
btree
(
project_id
)
WHERE
(
issue_type
=
1
);
CREATE
UNIQUE
INDEX
index_jira_connect_installations_on_client_key
ON
jira_connect_installations
USING
btree
(
client_key
);
CREATE
INDEX
index_jira_connect_subscriptions_on_namespace_id
ON
jira_connect_subscriptions
USING
btree
(
namespace_id
);
...
...
@@ -22852,6 +22872,9 @@ ALTER TABLE ONLY gpg_signatures
ALTER
TABLE
ONLY
vulnerability_user_mentions
ADD
CONSTRAINT
fk_rails_1a41c485cd
FOREIGN
KEY
(
vulnerability_id
)
REFERENCES
vulnerabilities
(
id
)
ON
DELETE
CASCADE
;
ALTER
TABLE
ONLY
issuable_slas
ADD
CONSTRAINT
fk_rails_1b8768cd63
FOREIGN
KEY
(
issue_id
)
REFERENCES
issues
(
id
)
ON
DELETE
CASCADE
;
ALTER
TABLE
ONLY
board_assignees
ADD
CONSTRAINT
fk_rails_1c0ff59e82
FOREIGN
KEY
(
assignee_id
)
REFERENCES
users
(
id
)
ON
DELETE
CASCADE
;
...
...
doc/api/graphql/reference/gitlab_schema.graphql
View file @
63754789
...
...
@@ -6725,6 +6725,11 @@ type EpicIssue implements CurrentUserTodos & Noteable {
"""
severity
:
IssuableSeverity
"""
Timestamp
of
when
the
issue
SLA
expires
.
Returns
null
if
`
incident_sla_dev
`
feature
flag
is
disabled
.
"""
slaDueAt
:
Time
"""
State
of
the
issue
"""
...
...
@@ -8880,6 +8885,11 @@ type Issue implements CurrentUserTodos & Noteable {
"""
severity
:
IssuableSeverity
"""
Timestamp
of
when
the
issue
SLA
expires
.
Returns
null
if
`
incident_sla_dev
`
feature
flag
is
disabled
.
"""
slaDueAt
:
Time
"""
State
of
the
issue
"""
...
...
doc/api/graphql/reference/gitlab_schema.json
View file @
63754789
...
...
@@ -18532,6 +18532,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "slaDueAt",
"description": "Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
...
...
@@ -24220,6 +24234,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "slaDueAt",
"description": "Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
doc/api/graphql/reference/index.md
View file @
63754789
...
...
@@ -1066,6 +1066,7 @@ Relationship between an epic and an issue.
|
`relationPath`
| String | URI path of the epic-issue relation |
|
`relativePosition`
| Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
`severity`
| IssuableSeverity | Severity level of the incident |
|
`slaDueAt`
| Time | Timestamp of when the issue SLA expires. Returns null if
`incident_sla_dev`
feature flag is disabled. |
|
`state`
| IssueState! | State of the issue |
|
`statusPagePublishedIncident`
| Boolean | Indicates whether an issue is published to the status page |
|
`subscribed`
| Boolean! | Indicates the currently logged in user is subscribed to the issue |
...
...
@@ -1256,6 +1257,7 @@ Represents a recorded measurement (object count) for the Admins.
|
`reference`
| String! | Internal reference of the issue. Returned in shortened format by default |
|
`relativePosition`
| Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
|
`severity`
| IssuableSeverity | Severity level of the incident |
|
`slaDueAt`
| Time | Timestamp of when the issue SLA expires. Returns null if
`incident_sla_dev`
feature flag is disabled. |
|
`state`
| IssueState! | State of the issue |
|
`statusPagePublishedIncident`
| Boolean | Indicates whether an issue is published to the status page |
|
`subscribed`
| Boolean! | Indicates the currently logged in user is subscribed to the issue |
...
...
ee/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_sla_due_at.graphql
0 → 100644
View file @
63754789
query
getSlaDueAt
(
$iid
:
String
!,
$fullPath
:
ID
!)
{
project
(
fullPath
:
$fullPath
)
{
issue
(
iid
:
$iid
)
{
slaDueAt
}
}
}
ee/app/assets/javascripts/issue_show/components/incidents/incident_sla.vue
0 → 100644
View file @
63754789
<
script
>
import
{
GlIcon
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
formatTime
,
calculateRemainingMilliseconds
}
from
'
~/lib/utils/datetime_utility
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
getSlaDueAt
from
'
./graphql/queries/get_sla_due_at.graphql
'
;
export
default
{
components
:
{
GlIcon
,
TimeAgoTooltip
},
inject
:
[
'
fullPath
'
,
'
iid
'
,
'
slaFeatureAvailable
'
],
apollo
:
{
slaDueAt
:
{
query
:
getSlaDueAt
,
variables
()
{
return
{
fullPath
:
this
.
fullPath
,
iid
:
this
.
iid
,
};
},
update
(
data
)
{
return
data
?.
project
?.
issue
?.
slaDueAt
;
},
result
({
data
}
=
{})
{
const
slaDueAt
=
data
?.
project
?.
issue
?.
slaDueAt
;
this
.
$emit
(
'
update
'
,
Boolean
(
slaDueAt
));
},
error
()
{
createFlash
({
message
:
s__
(
'
Incident|There was an issue loading incident data. Please try again.
'
),
});
},
},
},
data
()
{
return
{
slaDueAt
:
null
,
};
},
computed
:
{
displayValue
()
{
const
time
=
formatTime
(
calculateRemainingMilliseconds
(
this
.
slaDueAt
));
// remove the seconds portion of the string
return
time
.
substring
(
0
,
time
.
length
-
3
);
},
},
};
</
script
>
<
template
>
<div
v-if=
"slaFeatureAvailable && slaDueAt"
>
<span
class=
"gl-font-weight-bold"
>
{{
s__
(
'
HighlightBar|Time to SLA:
'
)
}}
</span>
<span
class=
"gl-white-space-nowrap"
>
<gl-icon
name=
"timer"
/>
<time-ago-tooltip
:time=
"slaDueAt"
>
{{
displayValue
}}
</time-ago-tooltip>
</span>
</div>
</
template
>
ee/app/controllers/ee/projects/settings/operations_controller.rb
View file @
63754789
...
...
@@ -21,7 +21,7 @@ module EE
end
def
sla_feature_available?
::
Feature
.
enabled?
(
:incident_sla_dev
,
@project
)
&&
@project
.
feature_available?
(
:incident_sla
,
current_user
)
::
IncidentManagement
::
IncidentSla
.
available_for?
(
@project
)
end
def
track_tracing_external_url
...
...
ee/app/graphql/ee/resolvers/issues_resolver.rb
View file @
63754789
...
...
@@ -4,12 +4,24 @@ module EE
module
Resolvers
module
IssuesResolver
extend
ActiveSupport
::
Concern
extend
::
Gitlab
::
Utils
::
Override
prepended
do
argument
:iteration_id
,
::
GraphQL
::
ID_TYPE
.
to_list_type
,
required:
false
,
description:
'Iterations applied to the issue'
end
private
override
:preloads
def
preloads
super
.
merge
(
{
sla_due_at:
[
:issuable_sla
]
}
)
end
end
end
end
ee/app/graphql/ee/types/issue_type.rb
View file @
63754789
...
...
@@ -23,14 +23,15 @@ module EE
::
Gitlab
::
Graphql
::
Aggregations
::
Issues
::
LazyBlockAggregate
.
new
(
ctx
,
obj
.
id
)
}
field
:health_status
,
::
Types
::
HealthStatusEnum
,
null:
true
,
field
:health_status
,
::
Types
::
HealthStatusEnum
,
null:
true
,
description:
'Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.'
,
resolve:
->
(
obj
,
_
,
_
)
{
obj
.
supports_health_status?
?
obj
.
health_status
:
nil
}
field
:status_page_published_incident
,
GraphQL
::
BOOLEAN_TYPE
,
null:
true
,
description:
'Indicates whether an issue is published to the status page'
field
:sla_due_at
,
::
Types
::
TimeType
,
null:
true
,
description:
'Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.'
end
end
end
...
...
ee/app/helpers/ee/issuables_helper.rb
View file @
63754789
...
...
@@ -34,7 +34,8 @@ module EE
return
{}
unless
issuable
.
is_a?
(
Issue
)
super
.
merge
(
publishedIncidentUrl:
::
Gitlab
::
StatusPage
::
Storage
.
details_url
(
issuable
)
publishedIncidentUrl:
::
Gitlab
::
StatusPage
::
Storage
.
details_url
(
issuable
),
slaFeatureAvailable:
issuable
.
sla_available?
.
to_s
)
end
...
...
ee/app/helpers/ee/operations_helper.rb
View file @
63754789
...
...
@@ -58,7 +58,7 @@ module EE
end
def
sla_feature_available?
::
Feature
.
enabled?
(
:incident_sla_dev
,
@project
)
&&
@project
.
feature_available?
(
:incident_sla
,
current_user
)
::
IncidentManagement
::
IncidentSla
.
available_for?
(
@project
)
end
def
opsgenie_mvc_data
...
...
ee/app/models/concerns/ee/issuable.rb
View file @
63754789
...
...
@@ -20,5 +20,15 @@ module EE
def
weight_available?
supports_weight?
&&
project
&
.
feature_available?
(
:issue_weights
)
end
def
sla_available?
return
false
unless
::
IncidentManagement
::
IncidentSla
.
available_for?
(
project
)
supports_sla?
end
def
supports_sla?
incident?
end
end
end
ee/app/models/ee/issue.rb
View file @
63754789
...
...
@@ -49,6 +49,7 @@ module EE
belongs_to
:promoted_to_epic
,
class_name:
'Epic'
has_one
:status_page_published_incident
,
class_name:
'StatusPage::PublishedIncident'
,
inverse_of: :issue
has_one
:issuable_sla
has_many
:vulnerability_links
,
class_name:
'Vulnerabilities::IssueLink'
,
inverse_of: :issue
has_many
:related_vulnerabilities
,
through: :vulnerability_links
,
source: :vulnerability
...
...
ee/app/models/issuable_sla.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
class
IssuableSla
<
ApplicationRecord
belongs_to
:issue
,
optional:
false
validates
:due_at
,
presence:
true
end
ee/app/presenters/ee/issue_presenter.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
module
EE
module
IssuePresenter
extend
ActiveSupport
::
Concern
def
sla_due_at
return
unless
sla_available?
issuable_sla
&
.
due_at
end
end
end
ee/app/services/ee/issues/create_service.rb
View file @
63754789
...
...
@@ -20,6 +20,21 @@ module EE
end
end
end
override
:after_create
def
after_create
(
issue
)
super
add_issue_sla
(
issue
)
end
private
def
add_issue_sla
(
issue
)
return
unless
issue
.
sla_available?
::
IncidentManagement
::
Incidents
::
CreateSlaService
.
new
(
issue
,
current_user
).
execute
end
end
end
end
ee/app/services/incident_management/incidents/create_sla_service.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
module
IncidentManagement
module
Incidents
class
CreateSlaService
<
BaseService
def
initialize
(
issuable
,
current_user
)
super
(
issuable
.
project
,
current_user
)
@issuable
=
issuable
end
def
execute
return
not_enabled_success
unless
issuable
.
sla_available?
return
not_enabled_success
unless
incident_setting
&
.
sla_timer?
sla
=
issuable
.
build_issuable_sla
(
due_at:
issuable
.
created_at
+
incident_setting
.
sla_timer_minutes
.
minutes
)
return
success
(
sla:
sla
)
if
sla
.
save
error
(
sla
.
errors
&
.
full_messages
)
end
attr_reader
:issuable
private
def
not_enabled_success
ServiceResponse
.
success
(
message:
'SLA not enabled'
)
end
def
success
(
payload
)
ServiceResponse
.
success
(
payload:
payload
)
end
def
error
(
message
)
ServiceResponse
.
error
(
message:
message
)
end
def
incident_setting
@incident_setting
||=
project
.
incident_management_setting
end
end
end
end
ee/lib/incident_management/incident_sla.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
module
IncidentManagement
module
IncidentSla
class
<<
self
def
available_for?
(
project
)
::
Feature
.
enabled?
(
:incident_sla_dev
,
project
)
&&
project
.
feature_available?
(
:incident_sla
)
end
end
end
end
ee/spec/factories/issuable_sla.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
FactoryBot
.
define
do
factory
:issuable_sla
do
issue
due_at
{
1
.
hour
.
from_now
}
end
end
ee/spec/frontend/issue_show/components/incidents/incident_sla_spec.js
0 → 100644
View file @
63754789
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
merge
}
from
'
lodash
'
;
import
IncidentSla
from
'
ee/issue_show/components/incidents/incident_sla.vue
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
{
formatTime
}
from
'
~/lib/utils/datetime_utility
'
;
jest
.
mock
(
'
~/lib/utils/datetime_utility
'
);
const
defaultProvide
=
{
fullPath
:
'
test
'
,
iid
:
1
,
slaFeatureAvailable
:
true
};
describe
(
'
Incident SLA
'
,
()
=>
{
let
wrapper
;
const
mountComponent
=
options
=>
{
wrapper
=
shallowMount
(
IncidentSla
,
merge
(
{
data
()
{
return
{
slaDueAt
:
'
2020-01-01T00:00:00.000Z
'
};
},
provide
:
{
...
defaultProvide
},
},
options
,
),
);
};
beforeEach
(()
=>
{
formatTime
.
mockImplementation
(()
=>
'
12:34:56
'
);
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
});
const
findTimer
=
()
=>
wrapper
.
find
(
TimeAgoTooltip
);
it
(
'
does not render an SLA when no sla is present
'
,
()
=>
{
mountComponent
({
data
()
{
return
{
slaDueAt
:
null
};
},
});
expect
(
findTimer
().
exists
()).
toBe
(
false
);
});
it
(
'
renders an incident SLA when sla is present
'
,
()
=>
{
mountComponent
();
expect
(
findTimer
().
text
()).
toBe
(
'
12:34
'
);
});
it
(
'
renders a component when feature is available
'
,
()
=>
{
mountComponent
();
expect
(
wrapper
.
exists
()).
toBe
(
true
);
});
it
(
'
renders a blank component when feature is not available
'
,
()
=>
{
mountComponent
({
provide
:
{
...
defaultProvide
,
slaFeatureAvailable
:
false
,
},
});
expect
(
wrapper
.
html
()).
toBe
(
''
);
});
});
ee/spec/graphql/types/issue_type_spec.rb
View file @
63754789
...
...
@@ -4,14 +4,11 @@ require 'spec_helper'
RSpec
.
describe
GitlabSchema
.
types
[
'Issue'
]
do
it
{
expect
(
described_class
).
to
have_graphql_field
(
:epic
)
}
it
{
expect
(
described_class
).
to
have_graphql_field
(
:iteration
)
}
it
{
expect
(
described_class
).
to
have_graphql_field
(
:weight
)
}
it
{
expect
(
described_class
).
to
have_graphql_field
(
:health_status
)
}
it
{
expect
(
described_class
).
to
have_graphql_field
(
:blocked
)
}
it
{
expect
(
described_class
).
to
have_graphql_field
(
:sla_due_at
)
}
context
'N+1 queries'
do
let_it_be
(
:user
)
{
create
(
:user
)
}
...
...
ee/spec/models/ee/incident_management/project_incident_management_setting_spec.rb
View file @
63754789
...
...
@@ -6,7 +6,7 @@ RSpec.describe EE::IncidentManagement::ProjectIncidentManagementSetting do
let_it_be
(
:project
)
{
create
(
:project
,
:repository
,
create_templates: :issue
)
}
describe
'Validations'
do
describe
'validate
incident
SLA settings'
do
describe
'validate SLA settings'
do
subject
{
build
(
:project_incident_management_setting
,
sla_timer:
sla_timer
)
}
describe
'#sla_timer_minutes'
do
...
...
ee/spec/models/issuable_sla_spec.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
IssuableSla
do
describe
'associations'
do
it
{
is_expected
.
to
belong_to
(
:issue
).
required
}
end
describe
'validations'
do
it
{
is_expected
.
to
validate_presence_of
(
:due_at
)
}
end
end
ee/spec/models/issue_spec.rb
View file @
63754789
...
...
@@ -12,6 +12,7 @@ RSpec.describe Issue do
it
{
is_expected
.
to
have_many
(
:resource_weight_events
)
}
it
{
is_expected
.
to
have_many
(
:resource_iteration_events
)
}
it
{
is_expected
.
to
have_one
(
:issuable_sla
)
}
end
describe
'modules'
do
...
...
@@ -802,4 +803,31 @@ RSpec.describe Issue do
expect
(
incident
.
issue_type_supports?
(
:epics
)).
to
be
(
false
)
end
end
describe
'#sla_available?'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be_with_refind
(
:issue
)
{
create
(
:incident
,
project:
project
)
}
subject
{
issue
.
sla_available?
}
where
(
:feature_enabled
,
:incident_type
,
:license_available
,
:sla_available
)
do
false
|
true
|
true
|
false
true
|
false
|
true
|
false
true
|
true
|
false
|
false
true
|
true
|
true
|
true
end
with_them
do
before
do
stub_feature_flags
(
incident_sla_dev:
feature_enabled
)
stub_licensed_features
(
incident_sla:
license_available
)
issue_type
=
incident_type
?
'incident'
:
'issue'
issue
.
update
(
issue_type:
issue_type
)
end
it
'returns the expected value'
do
expect
(
subject
).
to
eq
(
sla_available
)
end
end
end
end
ee/spec/presenters/ee/issue_presenter_spec.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
IssuePresenter
do
describe
'#sla_due_at'
do
let_it_be
(
:incident
)
{
create
(
:incident
)
}
let_it_be
(
:issuable_sla
)
{
create
(
:issuable_sla
,
issue:
incident
)
}
subject
{
described_class
.
new
(
incident
).
present
.
sla_due_at
}
before
do
allow
(
incident
).
to
receive
(
:sla_available?
).
and_return
(
available
)
end
context
'issue sla available'
do
let
(
:available
)
{
true
}
it
{
is_expected
.
to
eq
(
issuable_sla
.
due_at
)
}
end
context
'issue sla not available'
do
let
(
:available
)
{
false
}
it
{
is_expected
.
to
eq
(
nil
)
}
end
end
end
ee/spec/services/incident_management/incidents/create_sla_service_spec.rb
0 → 100644
View file @
63754789
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
IncidentManagement
::
Incidents
::
CreateSlaService
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be_with_refind
(
:incident
)
{
create
(
:incident
,
project:
project
)
}
describe
'#execute'
do
subject
(
:create_issuable_sla_response
)
{
described_class
.
new
(
incident
,
user
).
execute
}
let
(
:response_payload_sla
)
{
create_issuable_sla_response
.
payload
[
:sla
]
}
let
(
:response_payload_message
)
{
create_issuable_sla_response
.
message
}
before_all
do
project
.
add_maintainer
(
user
)
end
before
do
stub_licensed_features
(
incident_sla:
true
)
end
shared_examples
'no issuable sla created'
do
it
'does not create the issuable sla'
do
expect
{
subject
}.
not_to
change
(
IssuableSla
,
:count
)
end
it
'does not return a sla'
do
expect
(
response_payload_sla
).
to
eq
(
nil
)
end
end
context
'incident setting not created'
do
it_behaves_like
'no issuable sla created'
end
context
'incident setting exists'
do
let
(
:sla_timer
)
{
true
}
let
(
:sla_timer_minutes
)
{
30
}
let!
(
:setting
)
{
create
(
:project_incident_management_setting
,
project:
project
,
sla_timer:
sla_timer
,
sla_timer_minutes:
sla_timer_minutes
)
}
context
'project does not have incident_sla feature'
do
before
do
stub_licensed_features
(
incident_sla:
false
)
end
it_behaves_like
'no issuable sla created'
end
context
'sla timer setting is disabled'
do
let
(
:sla_timer
)
{
false
}
it_behaves_like
'no issuable sla created'
end
it
'creates the issuable sla with the given offset'
,
:aggregate_failures
do
expect
{
subject
}.
to
change
(
IssuableSla
,
:count
)
offset_time
=
incident
.
created_at
+
setting
.
sla_timer_minutes
.
minutes
expect
(
response_payload_sla
.
due_at
).
to
eq
(
offset_time
)
end
it
'returns a success with the sla'
,
:aggregate_failures
do
expect
(
create_issuable_sla_response
.
success?
).
to
eq
(
true
)
expect
(
response_payload_sla
).
to
be_a
(
IssuableSla
)
end
context
'errors when saving'
do
before
do
allow_next_instance_of
(
IssuableSla
)
do
|
issuable_sla
|
allow
(
issuable_sla
).
to
receive
(
:save
).
and_return
(
false
)
errors
=
ActiveModel
::
Errors
.
new
(
issuable_sla
).
tap
{
|
e
|
e
.
add
(
:issue_id
,
'error message'
)
}
allow
(
issuable_sla
).
to
receive
(
:errors
).
and_return
(
errors
)
end
end
it
'does not create the issuable sla'
do
expect
{
subject
}.
not_to
change
(
IssuableSla
,
:count
)
end
it
'returns an error'
,
:aggregate_failures
do
expect
(
create_issuable_sla_response
.
error?
).
to
eq
(
true
)
expect
(
response_payload_message
).
to
include
(
'Issue error message'
)
end
end
end
end
end
lib/gitlab/import_export/project/import_export.yml
View file @
63754789
...
...
@@ -403,9 +403,15 @@ ee:
-
issues
:
-
epic_issue
:
-
:epic
-
:issuable_sla
-
protected_branches
:
-
:unprotect_access_levels
-
protected_environments
:
-
:deploy_access_levels
-
:service_desk_setting
-
:security_setting
included_attributes
:
issuable_sla
:
-
:issue
-
:due_at
locale/gitlab.pot
View file @
63754789
...
...
@@ -13280,6 +13280,9 @@ msgstr ""
msgid "HighlightBar|Original alert:"
msgstr ""
msgid "HighlightBar|Time to SLA:"
msgstr ""
msgid "History"
msgstr ""
...
...
@@ -13836,6 +13839,9 @@ msgstr ""
msgid "Incident|There was an issue loading alert data. Please try again."
msgstr ""
msgid "Incident|There was an issue loading incident data. Please try again."
msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
...
...
spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
View file @
63754789
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
merge
from
'
lodash/merge
'
;
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
HighlightBar
from
'
~/issue_show/components/incidents/highlight_bar.vue
'
;
import
{
formatDate
}
from
'
~/lib/utils/datetime_utility
'
;
...
...
@@ -16,12 +17,17 @@ describe('Highlight Bar', () => {
title
:
'
Alert 1
'
,
};
const
mountComponent
=
()
=>
{
wrapper
=
shallowMount
(
HighlightBar
,
{
propsData
:
{
alert
,
},
});
const
mountComponent
=
options
=>
{
wrapper
=
shallowMount
(
HighlightBar
,
merge
(
{
propsData
:
{
alert
},
provide
:
{
fullPath
:
'
test
'
,
iid
:
1
,
slaFeatureAvailable
:
true
},
},
options
,
),
);
};
beforeEach
(()
=>
{
...
...
@@ -37,22 +43,52 @@ describe('Highlight Bar', () => {
const
findLink
=
()
=>
wrapper
.
find
(
GlLink
);
it
(
'
renders a link to the alert page
'
,
()
=>
{
expect
(
findLink
().
exists
()).
toBe
(
true
);
expect
(
findLink
().
attributes
(
'
href
'
)).
toBe
(
alert
.
detailsUrl
);
expect
(
findLink
().
attributes
(
'
title
'
)).
toBe
(
alert
.
title
);
expect
(
findLink
().
text
()).
toBe
(
`#
${
alert
.
iid
}
`
);
describe
(
'
empty state
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
propsData
:
{
alert
:
null
}
});
});
it
(
'
renders a empty component
'
,
()
=>
{
expect
(
wrapper
.
isVisible
()).
toBe
(
false
);
});
});
it
(
'
renders formatted start time of the alert
'
,
()
=>
{
const
formattedDate
=
'
2020-05-29 UTC
'
;
formatDate
.
mockReturnValueOnce
(
formattedDate
);
mountComponent
();
expect
(
formatDate
).
toHaveBeenCalledWith
(
alert
.
startedAt
,
'
yyyy-mm-dd Z
'
);
expect
(
wrapper
.
text
()).
toContain
(
formattedDate
);
describe
(
'
alert present
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
renders a link to the alert page
'
,
()
=>
{
expect
(
findLink
().
exists
()).
toBe
(
true
);
expect
(
findLink
().
attributes
(
'
href
'
)).
toBe
(
alert
.
detailsUrl
);
expect
(
findLink
().
attributes
(
'
title
'
)).
toBe
(
alert
.
title
);
expect
(
findLink
().
text
()).
toBe
(
`#
${
alert
.
iid
}
`
);
});
it
(
'
renders formatted start time of the alert
'
,
()
=>
{
const
formattedDate
=
'
2020-05-29 UTC
'
;
formatDate
.
mockReturnValueOnce
(
formattedDate
);
mountComponent
();
expect
(
formatDate
).
toHaveBeenCalledWith
(
alert
.
startedAt
,
'
yyyy-mm-dd Z
'
);
expect
(
wrapper
.
text
()).
toContain
(
formattedDate
);
});
it
(
'
renders a number of alert events
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
alert
.
eventCount
);
});
});
it
(
'
renders a number of alert events
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
alert
.
eventCount
);
describe
(
'
when child data is present
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
data
()
{
return
{
hasChildData
:
true
};
},
});
});
it
(
'
renders the highlight bar component
'
,
()
=>
{
expect
(
wrapper
.
isVisible
()).
toBe
(
true
);
});
});
});
spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
View file @
63754789
...
...
@@ -57,7 +57,6 @@ describe('Incident Tabs component', () => {
it
(
'
does not show the alert details tab
'
,
()
=>
{
expect
(
findAlertDetailsComponent
().
exists
()).
toBe
(
false
);
expect
(
findHighlightBarComponent
().
exists
()).
toBe
(
false
);
});
});
...
...
spec/lib/gitlab/import_export/all_models.yml
View file @
63754789
...
...
@@ -30,6 +30,7 @@ issues:
-
metrics
-
timelogs
-
issuable_severity
-
issuable_sla
-
issue_assignees
-
closed_by
-
epic_issue
...
...
@@ -713,3 +714,5 @@ system_note_metadata:
-
description_version
status_page_published_incident
:
-
issue
issuable_sla
:
-
issue
spec/lib/gitlab/import_export/import_test_coverage_spec.rb
View file @
63754789
...
...
@@ -23,6 +23,7 @@ RSpec.describe 'Test coverage of the Project Import' do
project.issues.notes.events
project.issues.notes.events.push_event_payload
project.issues.milestone.events.push_event_payload
project.issues.issuable_sla
project.issues.issue_milestones
project.issues.issue_milestones.milestone
project.issues.resource_label_events.label.priorities
...
...
spec/lib/gitlab/import_export/safe_model_attributes.yml
View file @
63754789
...
...
@@ -855,3 +855,6 @@ ProjectSecuritySetting:
-
auto_fix_sast
-
created_at
-
updated_at
IssuableSla
:
-
issue_id
-
due_at
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