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
labels: [:labels],
assignees: [:assignees],
timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] }
customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status]
}
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
field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,
description: 'Customer relations contacts of the issue.'
field :escalation_status, Types::IncidentManagement::EscalationStatusEnum, null: true,
description: 'Escalation status of the issue.'
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
......@@ -167,6 +170,12 @@ module Types
def hidden?
object.hidden? if Feature.enabled?(:ban_user_feature_flag)
end
def escalation_status
return unless Feature.enabled?(:incident_escalations, object.project) && object.supports_escalation?
object.escalation_status&.status_name
end
end
end
......
......@@ -55,6 +55,7 @@ module Types
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::SetSeverity
mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::SetEscalationStatus
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::Issues::Move
mount_mutation Mutations::Labels::Create
......
......@@ -2962,6 +2962,27 @@ Input type: `IssueSetEscalationPolicyInput`
| <a id="mutationissuesetescalationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the 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`
Input type: `IssueSetIterationInput`
......@@ -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="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="epicissueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <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="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
......@@ -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="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="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
| <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="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
......@@ -16820,6 +16843,17 @@ Iteration ID wildcard values for issue creation.
| ----- | ----------- |
| <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`
Values for sorting issues.
......@@ -17,6 +17,7 @@ module Mutations
project = issue.project
authorize_escalation_status!(project)
check_feature_availability!(project, issue)
::Issues::UpdateService.new(
project: project,
......@@ -37,6 +38,12 @@ module Mutations
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
......@@ -24,14 +24,14 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do
end
context 'when the user can update the issue' do
before 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 do
before_all do
project.add_developer(user)
end
......@@ -47,6 +47,24 @@ RSpec.describe Mutations::Issues::SetEscalationPolicy do
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
context 'when passing escalation_policy_id as nil' do
let(:args) { { escalation_policy: nil } }
......
......@@ -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
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)
......
# 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
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
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|
expect(described_class).to have_graphql_field(field_name)
......@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do
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
# 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
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
let(:extra_iid_for_second_query) { issue_b.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