Commit d039a01e authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Peter Leitzen

Add alert counts by status to graphql

Adds support for a alert_management_alert_status_count query
to GraphQL project. This includes counts for each status of
alert for the query, as well as counts for a categories 'all'
and 'open' which includes both triggered and acknowledged events.
parent 9ab9113e
......@@ -2,6 +2,12 @@
module AlertManagement
class AlertsFinder
# @return [Hash<Integer,Integer>] Mapping of status id to count
# ex) { 0: 6, ...etc }
def self.counts_by_status(current_user, project, params = {})
new(current_user, project, params).execute.counts_by_status
end
def initialize(current_user, project, params)
@current_user = current_user
@project = project
......
# frozen_string_literal: true
module Resolvers
module AlertManagement
class AlertStatusCountsResolver < BaseResolver
type Types::AlertManagement::AlertStatusCountsType, null: true
def resolve(**args)
::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args)
end
end
end
end
......@@ -23,9 +23,9 @@ module Resolvers
def resolve(**args)
parent = object.respond_to?(:sync) ? object.sync : object
return AlertManagement::Alert.none if parent.nil?
return ::AlertManagement::Alert.none if parent.nil?
AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
end
end
end
# frozen_string_literal: true
# Service for managing alert counts and cache updates.
module Types
module AlertManagement
class AlertStatusCountsType < BaseObject
graphql_name 'AlertManagementAlertStatusCountsType'
description "Represents total number of alerts for the represented categories"
authorize :read_alert_management_alert
::Gitlab::AlertManagement::AlertStatusCounts::STATUSES.each_key do |status|
field status,
GraphQL::INT_TYPE,
null: true,
description: "Number of alerts with status #{status.upcase} for the project"
end
field :open,
GraphQL::INT_TYPE,
null: true,
description: 'Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project'
field :all,
GraphQL::INT_TYPE,
null: true,
description: 'Total number of alerts for the project'
end
end
end
......@@ -218,6 +218,12 @@ module Types
description: 'A single Alert Management alert of the project',
resolver: Resolvers::AlertManagementAlertResolver.single
field :alert_management_alert_status_counts,
Types::AlertManagement::AlertStatusCountsType,
null: true,
description: 'Counts of alerts by status for the project',
resolver: Resolvers::AlertManagement::AlertStatusCountsResolver
field :releases,
Types::ReleaseType.connection_type,
null: true,
......
......@@ -106,6 +106,8 @@ module AlertManagement
scope :order_severity, -> (sort_order) { order(severity: sort_order) }
scope :order_status, -> (sort_order) { order(status: sort_order) }
scope :counts_by_status, -> { group(:status).count }
def self.sort_by_attribute(method)
case method.to_s
when 'start_time_asc' then order_start_time(:asc)
......
---
title: Add alert counts by status to GraphQL API
merge_request: 31818
author:
type: changed
......@@ -348,6 +348,41 @@ enum AlertManagementAlertSort {
updated_desc
}
"""
Represents total number of alerts for the represented categories
"""
type AlertManagementAlertStatusCountsType {
"""
Number of alerts with status ACKNOWLEDGED for the project
"""
acknowledged: Int
"""
Total number of alerts for the project
"""
all: Int
"""
Number of alerts with status IGNORED for the project
"""
ignored: Int
"""
Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project
"""
open: Int
"""
Number of alerts with status RESOLVED for the project
"""
resolved: Int
"""
Number of alerts with status TRIGGERED for the project
"""
triggered: Int
}
"""
Alert severity values
"""
......@@ -7384,6 +7419,11 @@ type Project {
statuses: [AlertManagementStatus!]
): AlertManagementAlert
"""
Counts of alerts by status for the project
"""
alertManagementAlertStatusCounts: AlertManagementAlertStatusCountsType
"""
Alert Management alerts of the project
"""
......
......@@ -855,6 +855,103 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementAlertStatusCountsType",
"description": "Represents total number of alerts for the represented categories",
"fields": [
{
"name": "acknowledged",
"description": "Number of alerts with status ACKNOWLEDGED for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "all",
"description": "Total number of alerts for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ignored",
"description": "Number of alerts with status IGNORED for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "open",
"description": "Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resolved",
"description": "Number of alerts with status RESOLVED for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "triggered",
"description": "Number of alerts with status TRIGGERED for the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "AlertManagementSeverity",
......@@ -22090,6 +22187,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementAlertStatusCounts",
"description": "Counts of alerts by status for the project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementAlertStatusCountsType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementAlerts",
"description": "Alert Management alerts of the project",
......@@ -68,6 +68,19 @@ Describes an alert from the project's Alert Management
| `title` | String | Title of the alert |
| `updatedAt` | Time | Timestamp the alert was last updated |
## AlertManagementAlertStatusCountsType
Represents total number of alerts for the represented categories
| Name | Type | Description |
| --- | ---- | ---------- |
| `acknowledged` | Int | Number of alerts with status ACKNOWLEDGED for the project |
| `all` | Int | Total number of alerts for the project |
| `ignored` | Int | Number of alerts with status IGNORED for the project |
| `open` | Int | Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project |
| `resolved` | Int | Number of alerts with status RESOLVED for the project |
| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
## AwardEmoji
An emoji awarded by a user.
......@@ -1115,6 +1128,7 @@ Information about pagination in a connection.
| Name | Type | Description |
| --- | ---- | ---------- |
| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project |
| `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project |
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
......
# frozen_string_literal: true
module Gitlab
module AlertManagement
# Represents counts of each status or category of statuses
class AlertStatusCounts
include Gitlab::Utils::StrongMemoize
STATUSES = ::AlertManagement::Alert::STATUSES
attr_reader :project
def self.declarative_policy_class
'AlertManagement::AlertPolicy'
end
def initialize(current_user, project, params)
@project = project
@current_user = current_user
@params = params
end
# Define method for each status
STATUSES.each_key do |status|
define_method(status) { counts[status] }
end
def open
counts[:triggered] + counts[:acknowledged]
end
def all
counts.values.sum # rubocop:disable CodeReuse/ActiveRecord
end
private
attr_reader :current_user, :params
def counts
strong_memoize(:counts) do
Hash.new(0).merge(counts_by_status)
end
end
def counts_by_status
::AlertManagement::AlertsFinder
.counts_by_status(current_user, project, params)
.transform_keys { |status_id| STATUSES.key(status_id) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::AlertManagement::AlertStatusCountsResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:args) { {} }
subject { resolve_alert_status_counts(args) }
it { is_expected.to be_a(Gitlab::AlertManagement::AlertStatusCounts) }
specify { expect(subject.project).to eq(project) }
private
def resolve_alert_status_counts(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AlertManagementAlertStatusCountsType'] do
specify { expect(described_class.graphql_name).to eq('AlertManagementAlertStatusCountsType') }
it 'exposes the expected fields' do
expected_fields = %i[
all
open
triggered
acknowledged
resolved
ignored
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
......@@ -25,6 +25,7 @@ describe GitlabSchema.types['Project'] do
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::AlertManagement::AlertStatusCounts do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, :ignored, project: project) }
let_it_be(:alert_3) { create(:alert_management_alert) }
let(:params) { {} }
describe '#execute' do
subject(:counts) { described_class.new(current_user, project, params) }
context 'for an unauthorized user' do
it 'returns zero for all statuses' do
expect(counts.open).to eq(0)
expect(counts.all).to eq(0)
AlertManagement::Alert::STATUSES.each_key do |status|
expect(counts.send(status)).to eq(0)
end
end
end
context 'for an authorized user' do
before do
project.add_developer(current_user)
end
it 'returns the correct counts for each status' do
expect(counts.open).to eq(0)
expect(counts.all).to eq(2)
expect(counts.resolved).to eq(1)
expect(counts.ignored).to eq(1)
expect(counts.triggered).to eq(0)
expect(counts.acknowledged).to eq(0)
end
context 'when filtering params are included' do
let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
it 'returns the correct counts for each status' do
expect(counts.open).to eq(0)
expect(counts.all).to eq(1)
expect(counts.resolved).to eq(1)
expect(counts.ignored).to eq(0)
expect(counts.triggered).to eq(0)
expect(counts.acknowledged).to eq(0)
end
end
end
end
end
......@@ -125,14 +125,14 @@ describe AlertManagement::Alert do
describe 'scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, :resolved, project: project) }
let_it_be(:alert_3) { create(:alert_management_alert, :ignored, project: project) }
let_it_be(:triggered_alert) { create(:alert_management_alert, project: project) }
let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project) }
let_it_be(:ignored_alert) { create(:alert_management_alert, :ignored, project: project) }
describe '.for_iid' do
subject { AlertManagement::Alert.for_iid(alert_1.iid) }
subject { AlertManagement::Alert.for_iid(triggered_alert.iid) }
it { is_expected.to match_array(alert_1) }
it { is_expected.to match_array(triggered_alert) }
end
describe '.for_status' do
......@@ -140,26 +140,36 @@ describe AlertManagement::Alert do
subject { AlertManagement::Alert.for_status(status) }
it { is_expected.to match_array(alert_2) }
it { is_expected.to match_array(resolved_alert) }
context 'with multiple statuses' do
let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
it { is_expected.to match_array([alert_2, alert_3]) }
it { is_expected.to match_array([resolved_alert, ignored_alert]) }
end
end
end
describe '.for_fingerprint' do
let_it_be(:fingerprint) { SecureRandom.hex }
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
let_it_be(:alert_3) { create(:alert_management_alert, fingerprint: fingerprint) }
describe '.for_fingerprint' do
let_it_be(:fingerprint) { SecureRandom.hex }
let_it_be(:alert_with_fingerprint) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
let_it_be(:unrelated_alert_with_finger_print) { create(:alert_management_alert, fingerprint: fingerprint) }
subject { described_class.for_fingerprint(project, fingerprint) }
subject { described_class.for_fingerprint(project, fingerprint) }
it { is_expected.to contain_exactly(alert_with_fingerprint) }
end
it { is_expected.to contain_exactly(alert_1) }
describe '.counts_by_status' do
subject { described_class.counts_by_status }
it do
is_expected.to eq(
triggered_alert.status => 1,
resolved_alert.status => 1,
ignored_alert.status => 1
)
end
end
end
describe '.search' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'getting Alert Management Alert counts by status' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
let_it_be(:other_project_alert) { create(:alert_management_alert) }
let(:params) { {} }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('AlertManagementAlertStatusCountsType'.classify)}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementAlertStatusCounts', params, fields)
)
end
context 'with alert data' do
let(:alert_counts) { graphql_data.dig('project', 'alertManagementAlertStatusCounts') }
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(alert_counts).to be nil }
end
context 'with project permissions' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns the correct counts for each status' do
expect(alert_counts).to eq(
'open' => 1,
'all' => 2,
'triggered' => 1,
'acknowledged' => 0,
'resolved' => 1,
'ignored' => 0
)
end
end
end
end
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