Commit 5e39b8d2 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Bob Van Landuyt

Improve performance of querying alert assignees - attempt 2

parent 1ff90a73
...@@ -282,7 +282,9 @@ export default { ...@@ -282,7 +282,9 @@ export default {
}, },
getAssignees(assignees) { getAssignees(assignees) {
// TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405 // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405
return assignees?.length > 0 ? assignees[0]?.username : s__('AlertManagement|Unassigned'); return assignees.nodes?.length > 0
? assignees.nodes[0]?.username
: s__('AlertManagement|Unassigned');
}, },
handlePageChange(page) { handlePageChange(page) {
const { startCursor, endCursor } = this.alerts.pageInfo; const { startCursor, endCursor } = this.alerts.pageInfo;
......
...@@ -6,8 +6,10 @@ fragment AlertListItem on AlertManagementAlert { ...@@ -6,8 +6,10 @@ fragment AlertListItem on AlertManagementAlert {
startedAt startedAt
endedAt endedAt
eventCount eventCount
issueIid, issueIid
assignees { assignees {
username nodes {
}, username
}
}
} }
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Resolvers module Resolvers
module AlertManagement module AlertManagement
class AlertResolver < BaseResolver class AlertResolver < BaseResolver
include LooksAhead
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'IID of the alert. For example, "1"' description: 'IID of the alert. For example, "1"'
...@@ -22,11 +24,17 @@ module Resolvers ...@@ -22,11 +24,17 @@ module Resolvers
type Types::AlertManagement::AlertType, null: true type Types::AlertManagement::AlertType, null: true
def resolve(**args) def resolve_with_lookahead(**args)
parent = object.respond_to?(:sync) ? object.sync : object 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 apply_lookahead(::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute)
end
def preloads
{
assignees: [:assignees]
}
end end
end end
end end
......
...@@ -85,7 +85,7 @@ module Types ...@@ -85,7 +85,7 @@ module Types
description: 'Timestamp the alert was last updated' description: 'Timestamp the alert was last updated'
field :assignees, field :assignees,
[Types::UserType], Types::UserType.connection_type,
null: true, null: true,
description: 'Assignees of the alert' description: 'Assignees of the alert'
......
...@@ -216,6 +216,7 @@ module Types ...@@ -216,6 +216,7 @@ module Types
Types::AlertManagement::AlertType.connection_type, Types::AlertManagement::AlertType.connection_type,
null: true, null: true,
description: 'Alert Management alerts of the project', description: 'Alert Management alerts of the project',
extras: [:lookahead],
resolver: Resolvers::AlertManagement::AlertResolver resolver: Resolvers::AlertManagement::AlertResolver
field :alert_management_alert, field :alert_management_alert,
......
...@@ -172,7 +172,27 @@ type AlertManagementAlert { ...@@ -172,7 +172,27 @@ type AlertManagementAlert {
""" """
Assignees of the alert Assignees of the alert
""" """
assignees: [User!] assignees(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): UserConnection
""" """
Timestamp the alert was created Timestamp the alert was created
......
...@@ -486,20 +486,51 @@ ...@@ -486,20 +486,51 @@
"name": "assignees", "name": "assignees",
"description": "Assignees of the alert", "description": "Assignees of the alert",
"args": [ "args": [
{
], "name": "after",
"type": { "description": "Returns the elements in the list that come after the specified cursor.",
"kind": "LIST", "type": {
"name": null, "kind": "SCALAR",
"ofType": { "name": "String",
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null "ofType": null
} },
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
} }
],
"type": {
"kind": "OBJECT",
"name": "UserConnection",
"ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
...@@ -61,7 +61,6 @@ Describes an alert from the project's Alert Management ...@@ -61,7 +61,6 @@ Describes an alert from the project's Alert Management
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `assignees` | User! => Array | Assignees of the alert |
| `createdAt` | Time | Timestamp the alert was created | | `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert | | `description` | String | Description of the alert |
| `details` | JSON | Alert details | | `details` | JSON | Alert details |
......
...@@ -262,7 +262,7 @@ describe('AlertManagementList', () => { ...@@ -262,7 +262,7 @@ describe('AlertManagementList', () => {
findAssignees() findAssignees()
.at(1) .at(1)
.text(), .text(),
).toBe(mockAlerts[1].assignees[0].username); ).toBe(mockAlerts[1].assignees.nodes[0].username);
}); });
it('navigates to the detail page when alert row is clicked', () => { it('navigates to the detail page when alert row is clicked', () => {
...@@ -291,6 +291,7 @@ describe('AlertManagementList', () => { ...@@ -291,6 +291,7 @@ describe('AlertManagementList', () => {
startedAt: '2020-03-17T23:18:14.996Z', startedAt: '2020-03-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z', endedAt: '2020-04-17T23:18:14.996Z',
severity: 'high', severity: 'high',
assignees: { nodes: [] },
}, },
], ],
}, },
......
[ [
{ {
"iid": "1527542", "iid": "1527542",
"title": "SyntaxError: Invalid or unexpected token", "title": "SyntaxError: Invalid or unexpected token",
"severity": "CRITICAL", "severity": "CRITICAL",
"eventCount": 7, "eventCount": 7,
"createdAt": "2020-04-17T23:18:14.996Z", "createdAt": "2020-04-17T23:18:14.996Z",
"startedAt": "2020-04-17T23:18:14.996Z", "startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED", "status": "TRIGGERED",
"assignees": [] "assignees": { "nodes": [] }
}, },
{ {
"iid": "1527543", "iid": "1527543",
"title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert", "title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert",
"severity": "MEDIUM", "severity": "MEDIUM",
"eventCount": 1, "eventCount": 1,
"startedAt": "2020-04-17T23:18:14.996Z", "startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED", "status": "ACKNOWLEDGED",
"assignees": [{"username": "root"}] "assignees": { "nodes": [{ "username": "root" }] }
}, },
{ {
"iid": "1527544", "iid": "1527544",
"title": "SyntaxError: Invalid or unexpected token", "title": "SyntaxError: Invalid or unexpected token",
"severity": "LOW", "severity": "LOW",
"eventCount": 4, "eventCount": 4,
"startedAt": "2020-04-17T23:18:14.996Z", "startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED", "status": "RESOLVED",
"assignees": [{"username": "root"}] "assignees": { "nodes": [{ "username": "root" }] }
} }
] ]
# frozen_string_literal: true
require 'spec_helper'
describe 'getting Alert Management Alert Assignees' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:first_alert) { create(:alert_management_alert, project: project, assignees: [current_user]) }
let_it_be(:second_alert) { create(:alert_management_alert, project: project) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
iid
assignees {
nodes {
username
}
}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementAlerts', params, fields)
)
end
let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
let(:assignees) { alerts.map { |alert| [alert['iid'], alert['assignees']['nodes']] }.to_h }
let(:first_assignees) { assignees[first_alert.iid.to_s] }
let(:second_assignees) { assignees[second_alert.iid.to_s] }
before do
project.add_developer(current_user)
end
it 'returns the correct assignees' do
post_graphql(query, current_user: current_user)
expect(first_assignees.length).to eq(1)
expect(first_assignees.first).to include('username' => current_user.username)
expect(second_assignees).to be_empty
end
it 'applies appropriate filters for non-visible users' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(current_user, :read_user, current_user).and_return(false)
post_graphql(query, current_user: current_user)
expect(first_assignees).to be_empty
expect(second_assignees).to be_empty
end
it 'avoids N+1 queries' do
base_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: current_user)
end
# An N+1 would mean a new alert would increase the query count
third_alert = create(:alert_management_alert, project: project, assignees: [current_user])
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(base_count)
third_assignees = assignees[third_alert.iid.to_s]
expect(third_assignees.length).to eq(1)
expect(third_assignees.first).to include('username' => current_user.username)
end
context 'with alert_assignee flag disabled' do
before do
stub_feature_flags(alert_assignee: false)
end
it 'excludes assignees' do
post_graphql(query, current_user: current_user)
expect(first_assignees).to be_empty
expect(second_assignees).to be_empty
end
end
end
...@@ -15,7 +15,7 @@ describe 'getting Alert Management Alerts' do ...@@ -15,7 +15,7 @@ describe 'getting Alert Management Alerts' do
let(:fields) do let(:fields) do
<<~QUERY <<~QUERY
nodes { nodes {
#{all_graphql_fields_for('AlertManagementAlert'.classify)} #{all_graphql_fields_for('AlertManagementAlert', excluded: ['assignees'])}
} }
QUERY QUERY
end end
...@@ -75,8 +75,6 @@ describe 'getting Alert Management Alerts' do ...@@ -75,8 +75,6 @@ describe 'getting Alert Management Alerts' do
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
) )
expect(first_alert['assignees'].first).to include('username' => triggered_alert.assignees.first.username)
expect(second_alert).to include( expect(second_alert).to include(
'iid' => resolved_alert.iid.to_s, 'iid' => resolved_alert.iid.to_s,
'issueIid' => nil, 'issueIid' => nil,
...@@ -137,18 +135,5 @@ describe 'getting Alert Management Alerts' do ...@@ -137,18 +135,5 @@ describe 'getting Alert Management Alerts' do
end end
end end
end end
context 'with alert_assignee flag disabled' do
before do
stub_feature_flags(alert_assignee: false)
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'excludes assignees' do
expect(alerts.first['assignees']).to be_empty
end
end
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