Commit 9c587ae5 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Robert Speicher

Add GFM reference format for alerts

Adds references for alerts in GitLab Flavored
Markdown. The  prefix ^ will be regonized in
GFM as links to alerts (from Operations > Alerts).
Example usage might look like: ^123. Full URLs
will be formatted in the same way for users who
have the ability to view alerts on the project
(developer+).
parent ca0859c5
......@@ -12,6 +12,7 @@ module AlertManagement
include Gitlab::SQL::Pattern
include Presentable
include Gitlab::Utils::StrongMemoize
include Referable
STATUSES = {
triggered: 0,
......@@ -170,6 +171,25 @@ module AlertManagement
with_prometheus_alert.where(id: ids)
end
def self.reference_prefix
'^alert#'
end
def self.reference_pattern
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<alert>\d+)
}x
end
def self.link_reference_pattern
@link_reference_pattern ||= super("alert_management", /(?<alert>\d+)\/details(\#)?/)
end
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def prometheus?
monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]
end
......@@ -178,10 +198,10 @@ module AlertManagement
increment!(:events)
end
# required for todos (typically contains an identifier like issue iid)
# no-op; we could use iid, but we don't have a reference prefix
def to_reference(_from = nil, full: false)
''
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference_base(from, full: full)}#{reference}"
end
def execute_services
......
---
title: Add GFM reference format for alerts
merge_request: 40922
author:
type: added
......@@ -438,6 +438,11 @@ GFM recognizes the following:
| commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` |
| repository file references | `[README](doc/README)` | | |
| repository file line references | `[README](doc/README#L13)` | | |
| [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
For example, referencing an issue by using `#123` will format the output as a link
to issue number 123 with text `#123`. Likewise, a link to issue number 123 will be
recognized and formatted with text `#123`.
In addition to this, links to some objects are also recognized and formatted. Some examples of these are:
......
# frozen_string_literal: true
module Banzai
module Filter
class AlertReferenceFilter < IssuableReferenceFilter
self.reference_type = :alert
def self.object_class
AlertManagement::Alert
end
def self.object_sym
:alert
end
def parent_records(parent, ids)
parent.alert_management_alerts.where(iid: ids.to_a)
end
def url_for_object(alert, project)
::Gitlab::Routing.url_helpers.details_project_alert_management_url(
project,
alert.iid,
only_path: context[:only_path]
)
end
end
end
end
......@@ -59,7 +59,8 @@ module Banzai
Filter::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter,
Filter::LabelReferenceFilter,
Filter::MilestoneReferenceFilter
Filter::MilestoneReferenceFilter,
Filter::AlertReferenceFilter
]
end
......
......@@ -23,7 +23,8 @@ module Banzai
Filter::MergeRequestReferenceFilter,
Filter::SnippetReferenceFilter,
Filter::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter
Filter::CommitReferenceFilter,
Filter::AlertReferenceFilter
]
end
......
# frozen_string_literal: true
module Banzai
module ReferenceParser
class AlertParser < BaseParser
self.reference_type = :alert
def references_relation
AlertManagement::Alert
end
private
def can_read_reference?(user, alert, node)
can?(user, :read_alert_management_alert, alert)
end
end
end
end
......@@ -247,6 +247,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
expect(doc).to reference_commits
expect(doc).to reference_labels
expect(doc).to reference_milestones
expect(doc).to reference_alerts
end
aggregate_failures 'TaskListFilter' do
......
......@@ -245,6 +245,15 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Group milestone by name in quotes: <%= group_milestone.to_reference(format: :name) %>
- Group milestone by URL is ignore: <%= urls.milestone_url(group_milestone) %>
##### AlertReferenceFilter
- Alert: <%= alert.to_reference %>
- Alert in another project: <%= xalert.to_reference(project) %>
- Ignored in code: `<%= alert.to_reference %>`
- Ignored in links: [Link to <%= alert.to_reference %>](#alert-link)
- Alert by URL: <%= alert.details_url %>
- Link to alert by reference: [Alert](<%= alert.to_reference %>)
- Link to alert by URL: [Alert](<%= alert.details_url %>)
### Task Lists
- [ ] Incomplete task 1
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::AlertReferenceFilter do
include FilterSpecHelper
let_it_be(:project) { create(:project, :public) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
let_it_be(:reference) { alert.to_reference }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Alert #{reference}</#{elem}>"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'internal reference' do
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq alert.details_url
end
it 'links with adjacent text' do
doc = reference_filter("Alert (#{reference}.)")
expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
end
it 'ignores invalid alert IDs' do
exp = act = "Alert #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = reference_filter("Alert #{reference}")
expect(doc.css('a').first.attr('title')).to eq alert.title
end
it 'escapes the title attribute' do
allow(alert).to receive(:title).and_return(%{"></a>whatever<a title="})
doc = reference_filter("Alert #{reference}")
expect(doc.text).to eq "Alert #{reference}"
end
it 'includes default classes' do
doc = reference_filter("Alert #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-alert has-tooltip'
end
it 'includes a data-project attribute' do
doc = reference_filter("Alert #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-alert attribute' do
doc = reference_filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-alert')
expect(link.attr('data-alert')).to eq alert.id.to_s
end
it 'supports an :only_path context' do
doc = reference_filter("Alert #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.details_project_alert_management_url(project, alert.iid, only_path: true)
end
end
context 'cross-project / cross-namespace complete reference' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:alert) { create(:alert_management_alert, project: project2) }
let_it_be(:reference) { "#{project2.full_path}^alert##{alert.iid}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq alert.details_url
end
it 'link has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.css('a').first.text).to eql(reference)
end
it 'has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.text).to eql("See (#{reference}.)")
end
it 'ignores invalid alert IDs on the referenced project' do
exp = act = "See #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'cross-project / same-namespace complete reference' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:alert) { create(:alert_management_alert, project: project2) }
let_it_be(:reference) { "#{project2.full_path}^alert##{alert.iid}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq alert.details_url
end
it 'link has valid text' do
doc = reference_filter("See (#{project2.path}^alert##{alert.iid}.)")
expect(doc.css('a').first.text).to eql("#{project2.path}^alert##{alert.iid}")
end
it 'has valid text' do
doc = reference_filter("See (#{project2.path}^alert##{alert.iid}.)")
expect(doc.text).to eql("See (#{project2.path}^alert##{alert.iid}.)")
end
it 'ignores invalid alert IDs on the referenced project' do
exp = act = "See #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'cross-project shorthand reference' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:alert) { create(:alert_management_alert, project: project2) }
let_it_be(:reference) { "#{project2.path}^alert##{alert.iid}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq alert.details_url
end
it 'link has valid text' do
doc = reference_filter("See (#{project2.path}^alert##{alert.iid}.)")
expect(doc.css('a').first.text).to eql("#{project2.path}^alert##{alert.iid}")
end
it 'has valid text' do
doc = reference_filter("See (#{project2.path}^alert##{alert.iid}.)")
expect(doc.text).to eql("See (#{project2.path}^alert##{alert.iid}.)")
end
it 'ignores invalid alert IDs on the referenced project' do
exp = act = "See #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'cross-project URL reference' do
let_it_be(:namespace) { create(:namespace, name: 'cross-reference') }
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:alert) { create(:alert_management_alert, project: project2) }
let_it_be(:reference) { alert.details_url }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq alert.details_url
end
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(alert.to_reference(project))}</a>\.\)})
end
it 'ignores invalid alert IDs on the referenced project' do
act = "See #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
end
end
context 'group context' do
let_it_be(:group) { create(:group) }
it 'links to a valid reference' do
reference = "#{project.full_path}^alert##{alert.iid}"
result = reference_filter("See #{reference}", { project: nil, group: group } )
expect(result.css('a').first.attr('href')).to eq(alert.details_url)
end
it 'ignores internal references' do
exp = act = "See ^alert##{alert.iid}"
expect(reference_filter(act, project: nil, group: group).to_html).to eq exp
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::AlertParser do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:alert) { create(:alert_management_alert, project: project) }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before do
link['data-alert'] = alert.id.to_s
end
it_behaves_like "referenced feature visibility", "issues", "merge_requests" do
before do
project.add_developer(user) if enable_user?
end
end
end
end
describe '#referenced_by' do
describe 'when the link has a data-alert attribute' do
context 'using an existing alert ID' do
it 'returns an Array of alerts' do
link['data-alert'] = alert.id.to_s
expect(subject.referenced_by([link])).to eq([alert])
end
end
context 'using a non-existing alert ID' do
it 'returns an empty Array' do
link['data-alert'] = ''
expect(subject.referenced_by([link])).to eq([])
end
end
end
end
end
......@@ -332,8 +332,39 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '.reference_pattern' do
subject { described_class.reference_pattern }
it { is_expected.to match('gitlab-org/gitlab^alert#123') }
end
describe '.link_reference_pattern' do
subject { described_class.link_reference_pattern }
it { is_expected.to match(triggered_alert.details_url) }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/alert_management/123") }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab/-/alert_management/123") }
end
describe '.reference_valid?' do
using RSpec::Parameterized::TableSyntax
where(:ref, :result) do
'123456' | true
'1' | true
'-1' | false
nil | false
'123456891012345678901234567890' | false
end
with_them do
it { expect(described_class.reference_valid?(ref)).to eq(result) }
end
end
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq('') }
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
describe '#trigger' do
......
......@@ -87,6 +87,10 @@ class MarkdownFeature
@group_milestone ||= create(:milestone, name: 'group-milestone', group: group)
end
def alert
@alert ||= create(:alert_management_alert, project: project)
end
# Cross-references -----------------------------------------------------------
def xproject
......@@ -125,6 +129,10 @@ class MarkdownFeature
@xmilestone ||= create(:milestone, project: xproject)
end
def xalert
@xalert ||= create(:alert_management_alert, project: xproject)
end
def urls
Gitlab::Routing.url_helpers
end
......
......@@ -174,6 +174,15 @@ module MarkdownMatchers
end
end
# AlertReferenceFilter
matcher :reference_alerts do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-alert', count: 5)
end
end
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
......
# frozen_string_literal: true
RSpec.shared_examples "referenced feature visibility" do |*related_features|
let(:enable_user?) { false }
let(:feature_fields) do
related_features.map { |feature| (feature + "_access_level").to_sym }
end
......@@ -35,8 +36,11 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features|
end
context "when feature is enabled" do
# The project is public
# Allows implementing specs to enable finer-tuned permissions
let(:enable_user?) { true }
it "creates reference" do
# The project is public
set_features_fields_to(ProjectFeature::ENABLED)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
......
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