Commit d5106043 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Vitali Tatarintev

Add paging status field and mutation for incidents

Adds a status field in GraphQL for issues which
support escalations. Functionality available
behind the :incident_escalations feature flag.
parent 40832d47
# frozen_string_literal: true
module Mutations
module Issues
class SetEscalationStatus < Base
graphql_name 'IssueSetEscalationStatus'
argument :status, Types::IncidentManagement::EscalationStatusEnum,
required: true,
description: 'Set the escalation status.'
def resolve(project_path:, iid:, status:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
authorize_escalation_status!(project)
check_feature_availability!(project, issue)
::Issues::UpdateService.new(
project: project,
current_user: current_user,
params: { escalation_status: { status: status } }
).execute(issue)
{
issue: issue,
errors: errors_on_object(issue)
}
end
private
def authorize_escalation_status!(project)
return if Ability.allowed?(current_user, :update_escalation_status, project)
raise_resource_not_available_error!
end
def check_feature_availability!(project, issue)
return if Feature.enabled?(:incident_escalations, project) && issue.supports_escalation?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue'
end
end
end
end
...@@ -48,7 +48,8 @@ module Resolvers ...@@ -48,7 +48,8 @@ module Resolvers
labels: [:labels], labels: [:labels],
assignees: [:assignees], assignees: [:assignees],
timelogs: [:timelogs], timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] } customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status]
} }
end end
......
# frozen_string_literal: true
module Types
module IncidentManagement
class EscalationStatusEnum < BaseEnum
graphql_name 'IssueEscalationStatus'
description 'Issue escalation status values'
::IncidentManagement::IssuableEscalationStatus.status_names.each do |status|
value status.to_s.upcase, value: status, description: "#{::IncidentManagement::IssuableEscalationStatus::STATUS_DESCRIPTIONS[status]}."
end
end
end
end
...@@ -140,6 +140,9 @@ module Types ...@@ -140,6 +140,9 @@ module Types
field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true, field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,
description: 'Customer relations contacts of the issue.' description: 'Customer relations contacts of the issue.'
field :escalation_status, Types::IncidentManagement::EscalationStatusEnum, null: true,
description: 'Escalation status of the issue.'
def author def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end end
...@@ -167,6 +170,12 @@ module Types ...@@ -167,6 +170,12 @@ module Types
def hidden? def hidden?
object.hidden? if Feature.enabled?(:ban_user_feature_flag) object.hidden? if Feature.enabled?(:ban_user_feature_flag)
end end
def escalation_status
return unless Feature.enabled?(:incident_escalations, object.project) && object.supports_escalation?
object.escalation_status&.status_name
end
end end
end end
......
...@@ -55,6 +55,7 @@ module Types ...@@ -55,6 +55,7 @@ module Types
mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::SetSeverity mount_mutation Mutations::Issues::SetSeverity
mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::SetEscalationStatus
mount_mutation Mutations::Issues::Update mount_mutation Mutations::Issues::Update
mount_mutation Mutations::Issues::Move mount_mutation Mutations::Issues::Move
mount_mutation Mutations::Labels::Create mount_mutation Mutations::Labels::Create
......
...@@ -2962,6 +2962,27 @@ Input type: `IssueSetEscalationPolicyInput` ...@@ -2962,6 +2962,27 @@ Input type: `IssueSetEscalationPolicyInput`
| <a id="mutationissuesetescalationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationissuesetescalationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetescalationpolicyissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. | | <a id="mutationissuesetescalationpolicyissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.issueSetEscalationStatus`
Input type: `IssueSetEscalationStatusInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesetescalationstatusclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesetescalationstatusiid"></a>`iid` | [`String!`](#string) | IID of the issue to mutate. |
| <a id="mutationissuesetescalationstatusprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the issue to mutate is in. |
| <a id="mutationissuesetescalationstatusstatus"></a>`status` | [`IssueEscalationStatus!`](#issueescalationstatus) | Set the escalation status. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesetescalationstatusclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesetescalationstatuserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetescalationstatusissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.issueSetIteration` ### `Mutation.issueSetIteration`
Input type: `IssueSetIterationInput` Input type: `IssueSetIterationInput`
...@@ -10317,6 +10338,7 @@ Relationship between an epic and an issue. ...@@ -10317,6 +10338,7 @@ Relationship between an epic and an issue.
| <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. | | <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
| <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | | <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="epicissueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | | <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
...@@ -11500,6 +11522,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). ...@@ -11500,6 +11522,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| <a id="issueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. | | <a id="issueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. |
| <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. | | <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | | <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | | <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
...@@ -16820,6 +16843,17 @@ Iteration ID wildcard values for issue creation. ...@@ -16820,6 +16843,17 @@ Iteration ID wildcard values for issue creation.
| ----- | ----------- | | ----- | ----------- |
| <a id="issuecreationiterationwildcardidcurrent"></a>`CURRENT` | Current iteration. | | <a id="issuecreationiterationwildcardidcurrent"></a>`CURRENT` | Current iteration. |
### `IssueEscalationStatus`
Issue escalation status values.
| Value | Description |
| ----- | ----------- |
| <a id="issueescalationstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. |
| <a id="issueescalationstatusignored"></a>`IGNORED` | No action will be taken. |
| <a id="issueescalationstatusresolved"></a>`RESOLVED` | The problem has been addressed. |
| <a id="issueescalationstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
### `IssueSort` ### `IssueSort`
Values for sorting issues. Values for sorting issues.
...@@ -17,6 +17,7 @@ module Mutations ...@@ -17,6 +17,7 @@ module Mutations
project = issue.project project = issue.project
authorize_escalation_status!(project) authorize_escalation_status!(project)
check_feature_availability!(project, issue)
::Issues::UpdateService.new( ::Issues::UpdateService.new(
project: project, project: project,
...@@ -37,6 +38,12 @@ module Mutations ...@@ -37,6 +38,12 @@ module Mutations
raise_resource_not_available_error! raise_resource_not_available_error!
end end
def check_feature_availability!(project, issue)
return if Feature.enabled?(:incident_escalations, project) && issue.supports_escalation?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue'
end
end end
end end
end end
...@@ -24,14 +24,14 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do ...@@ -24,14 +24,14 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do
end end
context 'when the user can update the issue' do context 'when the user can update the issue' do
before do before_all do
project.add_reporter(user) project.add_reporter(user)
end end
it_behaves_like 'permission level for issue mutation is correctly verified', true it_behaves_like 'permission level for issue mutation is correctly verified', true
context 'when the user can update the escalation status' do context 'when the user can update the escalation status' do
before do before_all do
project.add_developer(user) project.add_developer(user)
end end
...@@ -47,6 +47,24 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do ...@@ -47,6 +47,24 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do
expect(result[:errors]).not_to be_empty expect(result[:errors]).not_to be_empty
end end
context 'with non-incident issue is provided' do
let_it_be(:issue) { create(:issue, project: project) }
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
end
end
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
end
end
context 'when passing escalation_policy_id as nil' do context 'when passing escalation_policy_id as nil' do
let(:args) { { escalation_policy: nil } } let(:args) { { escalation_policy: nil } }
......
...@@ -41,9 +41,27 @@ RSpec.describe 'Setting the escalation policy of an issue' do ...@@ -41,9 +41,27 @@ RSpec.describe 'Setting the escalation policy of an issue' do
context 'when user does not have permission to edit the escalation status' do context 'when user does not have permission to edit the escalation status' do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
before_all do
project.add_reporter(user)
end
it_behaves_like 'a mutation that returns a top-level access error' it_behaves_like 'a mutation that returns a top-level access error'
end end
context 'with non-incident issue is provided' do
let_it_be(:issue) { create(:issue, project: project) }
it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
end
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
end
it 'sets given escalation_policy to the escalation status for the issue' do it 'sets given escalation_policy to the escalation status for the issue' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::SetEscalationStatus do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue, reload: true) { create(:incident, project: project) }
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
let(:status) { :acknowledged }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let(:args) { { status: status } }
let(:mutated_issue) { result[:issue] }
subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) }
it_behaves_like 'permission level for issue mutation is correctly verified', true
context 'when the user can update the issue' do
before_all do
project.add_reporter(user)
end
it_behaves_like 'permission level for issue mutation is correctly verified', true
context 'when the user can update the escalation status' do
before_all do
project.add_developer(user)
end
it 'returns the issue with the escalation policy' do
expect(mutated_issue).to eq(issue)
expect(mutated_issue.escalation_status.status_name).to eq(status)
expect(result[:errors]).to be_empty
end
it 'returns errors when issue update fails' do
issue.update_column(:author_id, nil)
expect(result[:errors]).not_to be_empty
end
context 'with non-incident issue is provided' do
let_it_be(:issue) { create(:issue, project: project) }
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
end
end
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it 'raises an error' do
expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['IssueEscalationStatus'] do
specify { expect(described_class.graphql_name).to eq('IssueEscalationStatus') }
describe 'statuses' do
using RSpec::Parameterized::TableSyntax
where(:status_name, :status_value) do
'TRIGGERED' | :triggered
'ACKNOWLEDGED' | :acknowledged
'RESOLVED' | :resolved
'IGNORED' | :ignored
'INVALID' | nil
end
with_them do
it 'exposes a status with the correct value' do
expect(described_class.values[status_name]&.value).to eq(status_value)
end
end
end
end
...@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do ...@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to design_collection alert_management_alert severity current_user_todos moved moved_to
create_note_email timelogs project_id customer_relations_contacts] create_note_email timelogs project_id customer_relations_contacts escalation_status]
fields.each do |field_name| fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name) expect(described_class).to have_graphql_field(field_name)
...@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do ...@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do
end end
end end
end end
describe 'escalation_status' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue, reload: true) { create(:issue, project: project) }
let(:execute) { GitlabSchema.execute(query, context: { current_user: user }).as_json }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
issue(iid: "#{issue.iid}") {
escalationStatus
}
}
}
)
end
subject(:status) { execute.dig('data', 'project', 'issue', 'escalationStatus') }
it { is_expected.to be_nil }
context 'for an incident' do
before do
issue.update!(issue_type: Issue.issue_types[:incident])
end
it { is_expected.to be_nil }
context 'with an escalation status record' do
let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
it { is_expected.to eq(escalation_status.status_name.to_s.upcase) }
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it { is_expected.to be_nil }
end
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting the escalation status of an incident' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:incident, project: project) }
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
let_it_be(:user) { create(:user) }
let(:status) { 'ACKNOWLEDGED' }
let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } }
let(:current_user) { user }
let(:mutation) do
graphql_mutation(:issue_set_escalation_status, input) do
<<~QL
clientMutationId
errors
issue {
iid
escalationStatus
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) }
before_all do
project.add_developer(user)
end
context 'when user does not have permission to edit the escalation status' do
let(:current_user) { create(:user) }
before_all do
project.add_reporter(user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'with non-incident issue is provided' do
let_it_be(:issue) { create(:issue, project: project) }
it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
end
context 'with feature disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
end
it 'sets given escalation_policy to the escalation status for the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(mutation_response['issue']['escalationStatus']).to eq(status)
expect(escalation_status.reload.status_name).to eq(:acknowledged)
end
context 'when status argument is not given' do
let(:input) { {} }
it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) }
end
end
context 'when status argument is invalid' do
let(:status) { 'INVALID' }
it_behaves_like 'an invalid argument to the mutation', argument_name: :status
end
end
...@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do ...@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do
end end
end end
context 'when fetching escalation status' do
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
let(:fields) do
<<~QUERY
edges {
node {
id
escalationStatus
}
}
QUERY
end
before do
issue_a.update!(issue_type: Issue.issue_types[:incident])
end
it 'returns the escalation status values' do
post_graphql(query, current_user: current_user)
statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
end
it 'avoids N+1 queries', :aggregate_failures do
base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
new_incident = create(:incident, project: project)
create(:incident_management_issuable_escalation_status, issue: new_incident)
expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
end
end
describe 'N+1 query checks' do describe 'N+1 query checks' do
let(:extra_iid_for_second_query) { issue_b.iid.to_s } let(:extra_iid_for_second_query) { issue_b.iid.to_s }
let(:search_params) { { iids: [issue_a.iid.to_s] } } let(:search_params) { { iids: [issue_a.iid.to_s] } }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment