Commit 130983b6 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch 'alert-assignee-list-view' into 'master'

Add assignee to alert management list view

See merge request gitlab-org/gitlab!32609
parents 7d649323 f67d38ec
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
GlDropdownItem, GlDropdownItem,
GlTabs, GlTabs,
GlTab, GlTab,
GlDeprecatedBadge as GlBadge, GlBadge,
} from '@gitlab/ui'; } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -77,6 +77,11 @@ export default { ...@@ -77,6 +77,11 @@ export default {
tdClass: `${tdClass} text-md-right`, tdClass: `${tdClass} text-md-right`,
sortable: true, sortable: true,
}, },
{
key: 'assignees',
label: s__('AlertManagement|Assignees'),
tdClass,
},
{ {
key: 'status', key: 'status',
thClass: 'w-15p', thClass: 'w-15p',
...@@ -237,6 +242,10 @@ export default { ...@@ -237,6 +242,10 @@ export default {
const { category, action, label } = trackAlertStatusUpdateOptions; const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status }); 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> </script>
...@@ -308,6 +317,12 @@ export default { ...@@ -308,6 +317,12 @@ export default {
<div class="gl-max-w-full text-truncate">{{ item.title }}</div> <div class="gl-max-w-full text-truncate">{{ item.title }}</div>
</template> </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 }"> <template #cell(status)="{ item }">
<gl-dropdown :text="$options.statuses[item.status]" class="w-100" right> <gl-dropdown :text="$options.statuses[item.status]" class="w-100" right>
<gl-dropdown-item <gl-dropdown-item
......
...@@ -6,5 +6,8 @@ fragment AlertListItem on AlertManagementAlert { ...@@ -6,5 +6,8 @@ fragment AlertListItem on AlertManagementAlert {
startedAt startedAt
endedAt endedAt
eventCount eventCount
issueIid issueIid,
assignees {
username
},
} }
...@@ -83,6 +83,11 @@ module Types ...@@ -83,6 +83,11 @@ module Types
Types::TimeType, Types::TimeType,
null: true, null: true,
description: 'Timestamp the alert was last updated' description: 'Timestamp the alert was last updated'
field :assignees,
[Types::UserType],
null: true,
description: 'Assignees of the alert'
end end
end end
end end
# frozen_string_literal: true
module AlertManagement
def self.table_name_prefix
'alert_management_'
end
end
# frozen_string_literal: true # frozen_string_literal: true
require_dependency 'alert_management'
module AlertManagement module AlertManagement
class Alert < ApplicationRecord class Alert < ApplicationRecord
include AtomicInternalId include AtomicInternalId
...@@ -23,9 +25,11 @@ module AlertManagement ...@@ -23,9 +25,11 @@ module AlertManagement
belongs_to :project belongs_to :project
belongs_to :issue, optional: true 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 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 ...@@ -24,6 +24,21 @@ CREATE SEQUENCE public.abuse_reports_id_seq
ALTER SEQUENCE public.abuse_reports_id_seq OWNED BY public.abuse_reports.id; 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 ( CREATE TABLE public.alert_management_alerts (
id bigint NOT NULL, id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
...@@ -7338,6 +7353,8 @@ ALTER SEQUENCE public.zoom_meetings_id_seq OWNED BY public.zoom_meetings.id; ...@@ -7338,6 +7353,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.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.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); ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
...@@ -7975,6 +7992,9 @@ ALTER TABLE ONLY public.zoom_meetings ALTER COLUMN id SET DEFAULT nextval('publi ...@@ -7975,6 +7992,9 @@ ALTER TABLE ONLY public.zoom_meetings ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.abuse_reports ALTER TABLE ONLY public.abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id); ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
...@@ -9100,6 +9120,10 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t ...@@ -9100,6 +9120,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_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 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); CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint ON public.alert_management_alerts USING btree (project_id, fingerprint);
...@@ -12306,6 +12330,9 @@ ALTER TABLE ONLY public.list_user_preferences ...@@ -12306,6 +12330,9 @@ ALTER TABLE ONLY public.list_user_preferences
ALTER TABLE ONLY public.board_labels ALTER TABLE ONLY public.board_labels
ADD CONSTRAINT fk_rails_9374a16edd FOREIGN KEY (board_id) REFERENCES public.boards(id) ON DELETE CASCADE; 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 ALTER TABLE ONLY public.scim_identities
ADD CONSTRAINT fk_rails_9421a0bffb FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_9421a0bffb FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
...@@ -12576,6 +12603,9 @@ ALTER TABLE ONLY public.group_group_links ...@@ -12576,6 +12603,9 @@ ALTER TABLE ONLY public.group_group_links
ALTER TABLE ONLY public.vulnerability_issue_links ALTER TABLE ONLY public.vulnerability_issue_links
ADD CONSTRAINT fk_rails_d459c19036 FOREIGN KEY (vulnerability_id) REFERENCES public.vulnerabilities(id) ON DELETE CASCADE; 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 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; ADD CONSTRAINT fk_rails_d496b088e9 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
...@@ -13991,6 +14021,9 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13991,6 +14021,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200519194042 20200519194042
20200520103514 20200520103514
20200521022725 20200521022725
20200521225327
20200521225337
20200521225346
20200525114553 20200525114553
20200525121014 20200525121014
20200526000407 20200526000407
......
...@@ -142,6 +142,11 @@ type AdminSidekiqQueuesDeleteJobsPayload { ...@@ -142,6 +142,11 @@ type AdminSidekiqQueuesDeleteJobsPayload {
Describes an alert from the project's Alert Management Describes an alert from the project's Alert Management
""" """
type AlertManagementAlert { type AlertManagementAlert {
"""
Assignees of the alert
"""
assignees: [User!]
""" """
Timestamp the alert was created Timestamp the alert was created
""" """
......
...@@ -394,6 +394,28 @@ ...@@ -394,6 +394,28 @@
"name": "AlertManagementAlert", "name": "AlertManagementAlert",
"description": "Describes an alert from the project's Alert Management", "description": "Describes an alert from the project's Alert Management",
"fields": [ "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", "name": "createdAt",
"description": "Timestamp the alert was created", "description": "Timestamp the alert was created",
...@@ -52,6 +52,7 @@ Describes an alert from the project's Alert Management ...@@ -52,6 +52,7 @@ 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 |
......
...@@ -1801,6 +1801,9 @@ msgstr "" ...@@ -1801,6 +1801,9 @@ msgstr ""
msgid "AlertManagement|Assign status" msgid "AlertManagement|Assign status"
msgstr "" msgstr ""
msgid "AlertManagement|Assignees"
msgstr ""
msgid "AlertManagement|Authorize external service" msgid "AlertManagement|Authorize external service"
msgstr "" msgstr ""
...@@ -1894,6 +1897,9 @@ msgstr "" ...@@ -1894,6 +1897,9 @@ msgstr ""
msgid "AlertManagement|Triggered" msgid "AlertManagement|Triggered"
msgstr "" msgstr ""
msgid "AlertManagement|Unassigned"
msgstr ""
msgid "AlertManagement|Unknown" msgid "AlertManagement|Unknown"
msgstr "" msgstr ""
......
...@@ -19,6 +19,12 @@ FactoryBot.define do ...@@ -19,6 +19,12 @@ FactoryBot.define do
issue issue
end end
trait :with_assignee do |alert|
after(:create) do |alert|
alert.alert_assignees.create(assignee: create(:user))
end
end
trait :with_fingerprint do trait :with_fingerprint do
fingerprint { SecureRandom.hex } fingerprint { SecureRandom.hex }
end end
...@@ -77,6 +83,7 @@ FactoryBot.define do ...@@ -77,6 +83,7 @@ FactoryBot.define do
trait :all_fields do trait :all_fields do
with_issue with_issue
with_assignee
with_fingerprint with_fingerprint
with_service with_service
with_monitoring_tool with_monitoring_tool
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
GlTab, GlTab,
GlDeprecatedBadge as GlBadge, GlBadge,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -42,6 +42,7 @@ describe('AlertManagementList', () => { ...@@ -42,6 +42,7 @@ describe('AlertManagementList', () => {
const findStatusFilterBadge = () => wrapper.findAll(GlBadge); const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo); const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findSeverityColumnHeader = () => wrapper.findAll('th').at(0); const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
...@@ -235,6 +236,34 @@ describe('AlertManagementList', () => { ...@@ -235,6 +236,34 @@ describe('AlertManagementList', () => {
).toBe('Critical'); ).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', () => { it('navigates to the detail page when alert row is clicked', () => {
mountComponent({ mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
"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": []
}, },
{ {
"iid": "1527543", "iid": "1527543",
...@@ -16,7 +17,8 @@ ...@@ -16,7 +17,8 @@
"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"}]
}, },
{ {
"iid": "1527544", "iid": "1527544",
...@@ -25,6 +27,7 @@ ...@@ -25,6 +27,7 @@
"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"}]
} }
] ]
...@@ -24,6 +24,7 @@ describe GitlabSchema.types['AlertManagementAlert'] do ...@@ -24,6 +24,7 @@ describe GitlabSchema.types['AlertManagementAlert'] do
details details
created_at created_at
updated_at updated_at
assignees
] ]
expect(described_class).to have_graphql_fields(*expected_fields) 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 ...@@ -6,6 +6,7 @@ describe AlertManagement::Alert do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) } it { is_expected.to belong_to(:issue) }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
end end
describe 'validations' do describe 'validations' do
......
...@@ -75,6 +75,8 @@ describe 'getting Alert Management Alerts' do ...@@ -75,6 +75,8 @@ 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,
......
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