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

Add alert assignee table, model, API support

Adds table and model for AlertManagement::AlertAssignee, which
will enable the association of users with alerts. This will
aide in triaging incoming alerts for a project.

This commit also adds the ability to read assignees for alerts
to the GraphQL API.
parent 9a2d788d
......@@ -10,7 +10,7 @@ import {
GlDropdownItem,
GlTabs,
GlTab,
GlDeprecatedBadge as GlBadge,
GlBadge,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
......@@ -77,6 +77,11 @@ export default {
tdClass: `${tdClass} text-md-right`,
sortable: true,
},
{
key: 'assignees',
label: s__('AlertManagement|Assignees'),
tdClass,
},
{
key: 'status',
thClass: 'w-15p',
......@@ -237,6 +242,10 @@ export default {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
getAssignees(assignees) {
// 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');
},
},
};
</script>
......@@ -308,6 +317,12 @@ export default {
<div class="gl-max-w-full text-truncate">{{ item.title }}</div>
</template>
<template #cell(assignees)="{ item }">
<div class="gl-max-w-full text-truncate" data-testid="assigneesField">
{{ getAssignees(item.assignees) }}
</div>
</template>
<template #cell(status)="{ item }">
<gl-dropdown :text="$options.statuses[item.status]" class="w-100" right>
<gl-dropdown-item
......
......@@ -6,5 +6,8 @@ fragment AlertListItem on AlertManagementAlert {
startedAt
endedAt
eventCount
issueIid
issueIid,
assignees {
username
},
}
......@@ -83,6 +83,11 @@ module Types
Types::TimeType,
null: true,
description: 'Timestamp the alert was last updated'
field :assignees,
[Types::UserType],
null: true,
description: 'Assignees of the alert'
end
end
end
# frozen_string_literal: true
module AlertManagement
def self.table_name_prefix
'alert_management_'
end
end
# frozen_string_literal: true
require_dependency 'alert_management'
module AlertManagement
class Alert < ApplicationRecord
include AtomicInternalId
......@@ -23,9 +25,11 @@ module AlertManagement
belongs_to :project
belongs_to :issue, optional: true
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
self.table_name = 'alert_management_alerts'
has_many :alert_assignees, inverse_of: :alert
has_many :assignees, through: :alert_assignees
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
sha_attribute :fingerprint
......
# frozen_string_literal: true
module AlertManagement
class AlertAssignee < ApplicationRecord
belongs_to :alert, inverse_of: :alert_assignees
belongs_to :assignee, class_name: 'User', foreign_key: :user_id
validates :alert, presence: true
validates :assignee, presence: true, uniqueness: { scope: :alert_id }
end
end
---
title: Add database and GraphQL support for alert assignees
merge_request: 32609
author:
type: added
# frozen_string_literal: true
class CreateAlertManagementAlertAssignees < ActiveRecord::Migration[6.0]
DOWNTIME = false
ALERT_INDEX_NAME = 'index_alert_assignees_on_alert_id'
UNIQUE_INDEX_NAME = 'index_alert_assignees_on_user_id_and_alert_id'
def up
create_table :alert_management_alert_assignees do |t|
t.bigint :user_id, null: false
t.bigint :alert_id, null: false
t.index :alert_id, name: ALERT_INDEX_NAME
t.index [:user_id, :alert_id], unique: true, name: UNIQUE_INDEX_NAME
end
end
def down
drop_table :alert_management_alert_assignees
end
end
# frozen_string_literal: true
class AddForeignKeyToUserIdOnAlertManagementAlertAssignees < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :alert_management_alert_assignees, :users, column: :user_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :alert_management_alert_assignees, column: :user_id
end
end
end
# frozen_string_literal: true
class AddForeignKeyToAlertIdOnAlertMangagementAlertAssignees < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :alert_management_alert_assignees, :alert_management_alerts, column: :alert_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :alert_management_alert_assignees, column: :alert_id
end
end
end
......@@ -24,6 +24,21 @@ CREATE SEQUENCE public.abuse_reports_id_seq
ALTER SEQUENCE public.abuse_reports_id_seq OWNED BY public.abuse_reports.id;
CREATE TABLE public.alert_management_alert_assignees (
id bigint NOT NULL,
user_id bigint NOT NULL,
alert_id bigint NOT NULL
);
CREATE SEQUENCE public.alert_management_alert_assignees_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.alert_management_alert_assignees_id_seq OWNED BY public.alert_management_alert_assignees.id;
CREATE TABLE public.alert_management_alerts (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -7321,6 +7336,8 @@ ALTER SEQUENCE public.zoom_meetings_id_seq OWNED BY public.zoom_meetings.id;
ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('public.abuse_reports_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_assignees_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alerts ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alerts_id_seq'::regclass);
ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
......@@ -7956,6 +7973,9 @@ ALTER TABLE ONLY public.zoom_meetings ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
......@@ -9078,6 +9098,10 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
CREATE INDEX index_abuse_reports_on_user_id ON public.abuse_reports USING btree (user_id);
CREATE INDEX index_alert_assignees_on_alert_id ON public.alert_management_alert_assignees USING btree (alert_id);
CREATE UNIQUE INDEX index_alert_assignees_on_user_id_and_alert_id ON public.alert_management_alert_assignees USING btree (user_id, alert_id);
CREATE INDEX index_alert_management_alerts_on_issue_id ON public.alert_management_alerts USING btree (issue_id);
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint ON public.alert_management_alerts USING btree (project_id, fingerprint);
......@@ -12276,6 +12300,9 @@ ALTER TABLE ONLY public.list_user_preferences
ALTER TABLE ONLY public.board_labels
ADD CONSTRAINT fk_rails_9374a16edd FOREIGN KEY (board_id) REFERENCES public.boards(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT fk_rails_93c0f6703b FOREIGN KEY (alert_id) REFERENCES public.alert_management_alerts(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.scim_identities
ADD CONSTRAINT fk_rails_9421a0bffb FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
......@@ -12546,6 +12573,9 @@ ALTER TABLE ONLY public.group_group_links
ALTER TABLE ONLY public.vulnerability_issue_links
ADD CONSTRAINT fk_rails_d459c19036 FOREIGN KEY (vulnerability_id) REFERENCES public.vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT fk_rails_d47570ac62 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.geo_hashed_storage_attachments_events
ADD CONSTRAINT fk_rails_d496b088e9 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -13960,6 +13990,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200519194042
20200520103514
20200521022725
20200521225327
20200521225337
20200521225346
20200525114553
20200525121014
20200526120714
......
......@@ -142,6 +142,11 @@ type AdminSidekiqQueuesDeleteJobsPayload {
Describes an alert from the project's Alert Management
"""
type AlertManagementAlert {
"""
Assignees of the alert
"""
assignees: [User!]
"""
Timestamp the alert was created
"""
......
......@@ -394,6 +394,28 @@
"name": "AlertManagementAlert",
"description": "Describes an alert from the project's Alert Management",
"fields": [
{
"name": "assignees",
"description": "Assignees of the alert",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp the alert was created",
......@@ -52,6 +52,7 @@ Describes an alert from the project's Alert Management
| Name | Type | Description |
| --- | ---- | ---------- |
| `assignees` | User! => Array | Assignees of the alert |
| `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert |
| `details` | JSON | Alert details |
......
......@@ -1798,6 +1798,9 @@ msgstr ""
msgid "AlertManagement|Assign status"
msgstr ""
msgid "AlertManagement|Assignees"
msgstr ""
msgid "AlertManagement|Authorize external service"
msgstr ""
......@@ -1891,6 +1894,9 @@ msgstr ""
msgid "AlertManagement|Triggered"
msgstr ""
msgid "AlertManagement|Unassigned"
msgstr ""
msgid "AlertManagement|Unknown"
msgstr ""
......
......@@ -19,6 +19,12 @@ FactoryBot.define do
issue
end
trait :with_assignee do |alert|
after(:create) do |alert|
alert.alert_assignees.create(assignee: create(:user))
end
end
trait :with_fingerprint do
fingerprint { SecureRandom.hex }
end
......@@ -77,6 +83,7 @@ FactoryBot.define do
trait :all_fields do
with_issue
with_assignee
with_fingerprint
with_service
with_monitoring_tool
......
......@@ -8,7 +8,7 @@ import {
GlDropdownItem,
GlIcon,
GlTab,
GlDeprecatedBadge as GlBadge,
GlBadge,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -42,6 +42,7 @@ describe('AlertManagementList', () => {
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
......@@ -235,6 +236,34 @@ describe('AlertManagementList', () => {
).toBe('Critical');
});
it('renders Unassigned when no assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
expect(
findAssignees()
.at(0)
.text(),
).toBe('Unassigned');
});
it('renders username(s) when assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
loading: false,
});
expect(
findAssignees()
.at(1)
.text(),
).toBe(mockAlerts[1].assignees[0].username);
});
it('navigates to the detail page when alert row is clicked', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
......
......@@ -7,7 +7,8 @@
"createdAt": "2020-04-17T23:18:14.996Z",
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED"
"status": "TRIGGERED",
"assignees": []
},
{
"iid": "1527543",
......@@ -16,7 +17,8 @@
"eventCount": 1,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED"
"status": "ACKNOWLEDGED",
"assignees": [{"username": "root"}]
},
{
"iid": "1527544",
......@@ -25,6 +27,7 @@
"eventCount": 4,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED"
"status": "RESOLVED",
"assignees": [{"username": "root"}]
}
]
......@@ -24,6 +24,7 @@ describe GitlabSchema.types['AlertManagementAlert'] do
details
created_at
updated_at
assignees
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertAssignee do
describe 'associations' do
it { is_expected.to belong_to(:alert) }
it { is_expected.to belong_to(:assignee) }
end
describe 'validations' do
let(:alert) { create(:alert_management_alert) }
let(:user) { create(:user) }
subject { alert.alert_assignees.build(assignee: user) }
it { is_expected.to validate_presence_of(:alert) }
it { is_expected.to validate_presence_of(:assignee) }
it { is_expected.to validate_uniqueness_of(:assignee).scoped_to(:alert_id) }
end
end
......@@ -6,6 +6,7 @@ describe AlertManagement::Alert do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
end
describe 'validations' do
......
......@@ -75,6 +75,8 @@ describe 'getting Alert Management Alerts' do
'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(
'iid' => resolved_alert.iid.to_s,
'issueIid' => nil,
......
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