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

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 4cba8a1f
...@@ -15,11 +15,13 @@ import { s__ } from '~/locale'; ...@@ -15,11 +15,13 @@ import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql'; import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants'; import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql'; import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue'; import AlertSidebar from './alert_sidebar.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar'); const containerEl = document.querySelector('.page-with-contextual-sidebar');
...@@ -47,7 +49,9 @@ export default { ...@@ -47,7 +49,9 @@ export default {
GlTable, GlTable,
TimeAgoTooltip, TimeAgoTooltip,
AlertSidebar, AlertSidebar,
SystemNote,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
alertId: { alertId: {
type: String, type: String,
...@@ -159,6 +163,9 @@ export default { ...@@ -159,6 +163,9 @@ export default {
const { category, action } = trackAlertsDetailsViewsOptions; const { category, action } = trackAlertsDetailsViewsOptions;
Tracking.event(category, action); Tracking.event(category, action);
}, },
alertRefresh() {
this.$apollo.queries.alert.refetch();
},
}, },
}; };
</script> </script>
...@@ -287,6 +294,13 @@ export default { ...@@ -287,6 +294,13 @@ export default {
</div> </div>
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div> <div class="gl-pl-2" data-testid="service">{{ alert.service }}</div>
</div> </div>
<template v-if="glFeatures.alertAssignee">
<div v-if="alert.notes" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
<system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
</ul>
</div>
</template>
</gl-tab> </gl-tab>
<gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle"> <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle">
<gl-table <gl-table
...@@ -309,6 +323,7 @@ export default { ...@@ -309,6 +323,7 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:alert="alert" :alert="alert"
:sidebar-collapsed="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar" @toggle-sidebar="toggleSidebar"
@alert-sidebar-error="handleAlertSidebarError" @alert-sidebar-error="handleAlertSidebarError"
/> />
......
...@@ -53,6 +53,7 @@ export default { ...@@ -53,6 +53,7 @@ export default {
v-if="glFeatures.alertAssignee" v-if="glFeatures.alertAssignee"
:project-path="projectPath" :project-path="projectPath"
:alert="alert" :alert="alert"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')" @toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)" @alert-sidebar-error="$emit('alert-sidebar-error', $event)"
/> />
......
...@@ -141,6 +141,7 @@ export default { ...@@ -141,6 +141,7 @@ export default {
}) })
.then(() => { .then(() => {
this.hideDropdown(); this.hideDropdown();
this.$emit('alert-refresh');
}) })
.catch(() => { .catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR); this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
......
<script>
import NoteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '~/lib/utils/common_utils';
export default {
components: {
NoteHeader,
},
props: {
note: {
type: Object,
required: true,
},
},
computed: {
noteAnchorId() {
return `note_${this.note?.id?.split('/').pop()}`;
},
iconHtml() {
return spriteIcon('user');
},
},
};
</script>
<template>
<li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.createdAt" :note-id="note.id">
<span v-html="note.bodyHtml"></span>
</note-header>
</div>
</div>
</div>
</li>
</template>
#import "~/graphql_shared/fragments/author.fragment.graphql"
fragment AlertNote on Note {
id
author {
id
state
...Author
}
body
bodyHtml
createdAt
discussion {
id
}
}
#import "./list_item.fragment.graphql" #import "./list_item.fragment.graphql"
#import "./alert_note.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert { fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem ...AlertListItem
...@@ -8,4 +9,9 @@ fragment AlertDetailItem on AlertManagementAlert { ...@@ -8,4 +9,9 @@ fragment AlertDetailItem on AlertManagementAlert {
description description
updatedAt updatedAt
details details
notes {
nodes {
...AlertNote
}
}
} }
#import "../fragments/detailItem.fragment.graphql" #import "../fragments/detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) { query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
......
...@@ -6,6 +6,8 @@ module Types ...@@ -6,6 +6,8 @@ module Types
graphql_name 'AlertManagementAlert' graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management" description "Describes an alert from the project's Alert Management"
implements(Types::Notes::NoteableType)
authorize :read_alert_management_alert authorize :read_alert_management_alert
field :iid, field :iid,
......
...@@ -19,6 +19,8 @@ module Types ...@@ -19,6 +19,8 @@ module Types
Types::SnippetType Types::SnippetType
when ::DesignManagement::Design when ::DesignManagement::Design
Types::DesignManagement::DesignType Types::DesignManagement::DesignType
when ::AlertManagement::Alert
Types::AlertManagement::AlertType
else else
raise "Unknown GraphQL type for #{object}" raise "Unknown GraphQL type for #{object}"
end end
......
...@@ -8,6 +8,7 @@ module AlertManagement ...@@ -8,6 +8,7 @@ module AlertManagement
include AtomicInternalId include AtomicInternalId
include ShaAttribute include ShaAttribute
include Sortable include Sortable
include Noteable
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
STATUSES = { STATUSES = {
...@@ -30,6 +31,9 @@ module AlertManagement ...@@ -30,6 +31,9 @@ module AlertManagement
has_many :alert_assignees, inverse_of: :alert has_many :alert_assignees, inverse_of: :alert
has_many :assignees, through: :alert_assignees has_many :assignees, through: :alert_assignees
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) } 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 AlertUserMention < UserMention
belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert'
belongs_to :note
end
end
...@@ -274,6 +274,10 @@ class Note < ApplicationRecord ...@@ -274,6 +274,10 @@ class Note < ApplicationRecord
noteable_type == "Snippet" noteable_type == "Snippet"
end end
def for_alert_mangement_alert?
noteable_type == 'AlertManagement::Alert'
end
def for_personal_snippet? def for_personal_snippet?
noteable.is_a?(PersonalSnippet) noteable.is_a?(PersonalSnippet)
end end
...@@ -396,7 +400,13 @@ class Note < ApplicationRecord ...@@ -396,7 +400,13 @@ class Note < ApplicationRecord
end end
def noteable_ability_name def noteable_ability_name
for_snippet? ? 'snippet' : noteable_type.demodulize.underscore if for_snippet?
'snippet'
elsif for_alert_mangement_alert?
'alert_management_alert'
else
noteable_type.demodulize.underscore
end
end end
def can_be_discussion_note? def can_be_discussion_note?
......
...@@ -19,9 +19,11 @@ module AlertManagement ...@@ -19,9 +19,11 @@ module AlertManagement
return error_no_updates if params.empty? return error_no_updates if params.empty?
filter_assignees filter_assignees
old_assignees = alert.assignees.to_a
if alert.update(params) if alert.update(params)
assign_todo process_assignement(old_assignees)
success success
else else
error(alert.errors.full_messages.to_sentence) error(alert.errors.full_messages.to_sentence)
...@@ -32,29 +34,10 @@ module AlertManagement ...@@ -32,29 +34,10 @@ module AlertManagement
attr_reader :alert, :current_user, :params attr_reader :alert, :current_user, :params
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def allowed? def allowed?
current_user.can?(:update_alert_management_alert, alert) current_user.can?(:update_alert_management_alert, alert)
end end
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def todo_service def todo_service
strong_memoize(:todo_service) do strong_memoize(:todo_service) do
TodoService.new TodoService.new
...@@ -76,6 +59,35 @@ module AlertManagement ...@@ -76,6 +59,35 @@ module AlertManagement
def error_no_updates def error_no_updates
error(_('Please provide attributes to update')) error(_('Please provide attributes to update'))
end end
# ----- Assignee-related behavior ------
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def process_assignement(old_assignees)
assign_todo
add_assignee_system_note(old_assignees)
end
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def add_assignee_system_note(old_assignees)
SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees)
end
end end
end end
end end
---
title: Add system note when assigning user to alert
merge_request: 33217
author:
type: added
# frozen_string_literal: true
class CreateAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :alert_management_alert_user_mentions do |t|
t.references :alert_management_alert, type: :bigint, index: false, null: false, foreign_key: { on_delete: :cascade }
t.bigint :note_id, null: true
t.integer :mentioned_users_ids, array: true
t.integer :mentioned_projects_ids, array: true
t.integer :mentioned_groups_ids, array: true
end
add_index :alert_management_alert_user_mentions, [:note_id], where: 'note_id IS NOT NULL', unique: true, name: 'index_alert_user_mentions_on_note_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id], where: 'note_id IS NULL', unique: true, name: 'index_alert_user_mentions_on_alert_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id, :note_id], unique: true, name: 'index_alert_user_mentions_on_alert_id_and_note_id'
end
end
# frozen_string_literal: true
class AddForeignKeyToAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :alert_management_alert_user_mentions, :notes, column: :note_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :alert_management_alert_user_mentions, column: :note_id
end
end
end
...@@ -37,6 +37,24 @@ CREATE SEQUENCE public.alert_management_alert_assignees_id_seq ...@@ -37,6 +37,24 @@ CREATE SEQUENCE public.alert_management_alert_assignees_id_seq
ALTER SEQUENCE public.alert_management_alert_assignees_id_seq OWNED BY public.alert_management_alert_assignees.id; ALTER SEQUENCE public.alert_management_alert_assignees_id_seq OWNED BY public.alert_management_alert_assignees.id;
CREATE TABLE public.alert_management_alert_user_mentions (
id bigint NOT NULL,
alert_management_alert_id bigint NOT NULL,
note_id bigint,
mentioned_users_ids integer[],
mentioned_projects_ids integer[],
mentioned_groups_ids integer[]
);
CREATE SEQUENCE public.alert_management_alert_user_mentions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.alert_management_alert_user_mentions_id_seq OWNED BY public.alert_management_alert_user_mentions.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,
...@@ -7401,6 +7419,8 @@ ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('publi ...@@ -7401,6 +7419,8 @@ ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('publi
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_alert_assignees ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_assignees_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alert_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_user_mentions_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);
...@@ -8045,6 +8065,9 @@ ALTER TABLE ONLY public.abuse_reports ...@@ -8045,6 +8065,9 @@ ALTER TABLE ONLY public.abuse_reports
ALTER TABLE ONLY public.alert_management_alert_assignees ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id); ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT alert_management_alert_user_mentions_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);
...@@ -9194,6 +9217,12 @@ CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint ...@@ -9194,6 +9217,12 @@ CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid); CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id) WHERE (note_id IS NULL);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id_and_note_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id, note_id);
CREATE UNIQUE INDEX index_alert_user_mentions_on_note_id ON public.alert_management_alert_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id); CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id);
CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id); CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id);
...@@ -12391,6 +12420,9 @@ ALTER TABLE ONLY public.design_user_mentions ...@@ -12391,6 +12420,9 @@ ALTER TABLE ONLY public.design_user_mentions
ALTER TABLE ONLY public.clusters_kubernetes_namespaces ALTER TABLE ONLY public.clusters_kubernetes_namespaces
ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_8e48eca0fe FOREIGN KEY (alert_management_alert_id) REFERENCES public.alert_management_alerts(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_daily_statistics ALTER TABLE ONLY public.project_daily_statistics
ADD CONSTRAINT fk_rails_8e549b272d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_8e549b272d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
...@@ -12769,6 +12801,9 @@ ALTER TABLE ONLY public.merge_request_blocks ...@@ -12769,6 +12801,9 @@ ALTER TABLE ONLY public.merge_request_blocks
ALTER TABLE ONLY public.protected_branch_unprotect_access_levels ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_e9eb8dc025 FOREIGN KEY (protected_branch_id) REFERENCES public.protected_branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_e9eb8dc025 FOREIGN KEY (protected_branch_id) REFERENCES public.protected_branches(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.ci_daily_report_results ALTER TABLE ONLY public.ci_daily_report_results
ADD CONSTRAINT fk_rails_ebc2931b90 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_ebc2931b90 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
...@@ -13873,6 +13908,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13873,6 +13908,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200527151413 20200527151413
20200527152116 20200527152116
20200527152657 20200527152657
20200527170649
20200527211000 20200527211000
20200528054112 20200528054112
20200528123703 20200528123703
...@@ -13886,6 +13922,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13886,6 +13922,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200604145731 20200604145731
20200604174544 20200604174544
20200604174558 20200604174558
20200605003204
20200608072931 20200608072931
20200608075553 20200608075553
20200609002841 20200609002841
......
...@@ -168,7 +168,7 @@ type AdminSidekiqQueuesDeleteJobsPayload { ...@@ -168,7 +168,7 @@ type AdminSidekiqQueuesDeleteJobsPayload {
""" """
Describes an alert from the project's Alert Management Describes an alert from the project's Alert Management
""" """
type AlertManagementAlert { type AlertManagementAlert implements Noteable {
""" """
Assignees of the alert Assignees of the alert
""" """
...@@ -209,6 +209,31 @@ type AlertManagementAlert { ...@@ -209,6 +209,31 @@ type AlertManagementAlert {
""" """
details: JSON details: JSON
"""
All discussions on this noteable
"""
discussions(
"""
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
): DiscussionConnection!
""" """
Timestamp the alert ended Timestamp the alert ended
""" """
...@@ -239,6 +264,31 @@ type AlertManagementAlert { ...@@ -239,6 +264,31 @@ type AlertManagementAlert {
""" """
monitoringTool: String monitoringTool: String
"""
All notes on this noteable
"""
notes(
"""
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
): NoteConnection!
""" """
Service the alert came from Service the alert came from
""" """
......
...@@ -577,6 +577,63 @@ ...@@ -577,6 +577,63 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "discussions",
"description": "All discussions on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"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": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiscussionConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "endedAt", "name": "endedAt",
"description": "Timestamp the alert ended", "description": "Timestamp the alert ended",
...@@ -673,6 +730,63 @@ ...@@ -673,6 +730,63 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "notes",
"description": "All notes on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"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": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "NoteConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "service", "name": "service",
"description": "Service the alert came from", "description": "Service the alert came from",
...@@ -760,7 +874,11 @@ ...@@ -760,7 +874,11 @@
], ],
"inputFields": null, "inputFields": null,
"interfaces": [ "interfaces": [
{
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
}
], ],
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
...@@ -23585,6 +23703,11 @@ ...@@ -23585,6 +23703,11 @@
"interfaces": null, "interfaces": null,
"enumValues": null, "enumValues": null,
"possibleTypes": [ "possibleTypes": [
{
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Design", "name": "Design",
...@@ -17,6 +17,7 @@ FactoryBot.define do ...@@ -17,6 +17,7 @@ FactoryBot.define do
factory :note_on_project_snippet, traits: [:on_project_snippet] factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet] factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :note_on_design, traits: [:on_design] factory :note_on_design, traits: [:on_design]
factory :note_on_alert, traits: [:on_alert]
factory :system_note, traits: [:system] factory :system_note, traits: [:system]
factory :discussion_note, class: 'DiscussionNote' factory :discussion_note, class: 'DiscussionNote'
...@@ -145,6 +146,10 @@ FactoryBot.define do ...@@ -145,6 +146,10 @@ FactoryBot.define do
end end
end end
trait :on_alert do
noteable { association(:alert_management_alert, project: project) }
end
trait :resolved do trait :resolved do
resolved_at { Time.now } resolved_at { Time.now }
resolved_by { association(:user) } resolved_by { association(:user) }
......
import { shallowMount } from '@vue/test-utils';
import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[1];
describe('Alert Details System Note', () => {
let wrapper;
function mountComponent({ stubs = {} } = {}) {
wrapper = shallowMount(SystemNote, {
propsData: {
note: { ...mockAlert.notes.nodes[0] },
},
stubs,
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('System notes', () => {
beforeEach(() => {
mountComponent({});
});
it('renders the correct system note', () => {
expect(wrapper.find('.note-wrapper').attributes('id')).toBe('note_1628');
});
});
});
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"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": { "nodes": [] } "assignees": { "nodes": [] },
"notes": { "nodes": [] }
}, },
{ {
"iid": "1527543", "iid": "1527543",
...@@ -18,7 +19,23 @@ ...@@ -18,7 +19,23 @@
"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": { "nodes": [{ "username": "root" }] } "assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1628",
"author": {
"id": "gid://gitlab/User/1",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
}, },
{ {
"iid": "1527544", "iid": "1527544",
...@@ -28,6 +45,22 @@ ...@@ -28,6 +45,22 @@
"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": { "nodes": [{ "username": "root" }] } "assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1629",
"author": {
"id": "gid://gitlab/User/2",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
} }
] ]
...@@ -25,6 +25,8 @@ describe GitlabSchema.types['AlertManagementAlert'] do ...@@ -25,6 +25,8 @@ describe GitlabSchema.types['AlertManagementAlert'] do
created_at created_at
updated_at updated_at
assignees assignees
notes
discussions
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
......
...@@ -17,6 +17,7 @@ describe Types::Notes::NoteableType do ...@@ -17,6 +17,7 @@ describe Types::Notes::NoteableType do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType) expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType) expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType) expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType)
end end
end end
end end
...@@ -7,6 +7,8 @@ describe AlertManagement::Alert do ...@@ -7,6 +7,8 @@ describe AlertManagement::Alert 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) } it { is_expected.to have_many(:assignees).through(:alert_assignees) }
it { is_expected.to have_many(:notes) }
it { is_expected.to have_many(:user_mentions) }
end end
describe 'validations' do describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertUserMention do
describe 'associations' do
it { is_expected.to belong_to(:alert_management_alert) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
...@@ -829,6 +829,10 @@ describe Note do ...@@ -829,6 +829,10 @@ describe Note do
it 'returns commit for a commit note' do it 'returns commit for a commit note' do
expect(build(:note_on_commit).noteable_ability_name).to eq('commit') expect(build(:note_on_commit).noteable_ability_name).to eq('commit')
end end
it 'returns alert_management_alert for an alert note' do
expect(build(:note_on_alert).noteable_ability_name).to eq('alert_management_alert')
end
end end
describe '#cache_markdown_field' do describe '#cache_markdown_field' do
......
...@@ -10,6 +10,8 @@ describe 'getting Alert Management Alerts' do ...@@ -10,6 +10,8 @@ describe 'getting Alert Management Alerts' do
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) } let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) } let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) } let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
let_it_be(:system_note) { create(:note_on_alert, noteable: triggered_alert, project: project) }
let(:params) { {} } let(:params) { {} }
let(:fields) do let(:fields) do
...@@ -75,6 +77,8 @@ describe 'getting Alert Management Alerts' do ...@@ -75,6 +77,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['notes']['nodes'].first).to include('id' => system_note.to_global_id.to_s)
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,
......
...@@ -40,6 +40,7 @@ describe AlertManagement::Alerts::UpdateService do ...@@ -40,6 +40,7 @@ describe AlertManagement::Alerts::UpdateService do
let(:params) { { title: nil } } let(:params) { { title: nil } }
it 'results in an error' do it 'results in an error' do
expect { response }.not_to change { alert.reload.notes.count }
expect(response).to be_error expect(response).to be_error
expect(response.message).to eq("Title can't be blank") expect(response.message).to eq("Title can't be blank")
end end
...@@ -72,6 +73,14 @@ describe AlertManagement::Alerts::UpdateService do ...@@ -72,6 +73,14 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success expect(response).to be_success
end end
it 'creates a system note for the assignment' do
expect { response }.to change { alert.reload.notes.count }.by(1)
end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
context 'with multiple users included' do context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } } let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
...@@ -80,10 +89,6 @@ describe AlertManagement::Alerts::UpdateService do ...@@ -80,10 +89,6 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success expect(response).to be_success
end end
end end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
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