Commit b5e1b76e authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Denys Mishunov

Expose url of incident on status page

In order to display in the UI when an issue
is published to a status page app, we want to
expose the url of the status detail page for
the issue is to the FE as part of the issue
request.
parent e520e164
......@@ -58,7 +58,12 @@ export default {
zoomMeetingUrl: {
type: String,
required: false,
default: null,
default: '',
},
publishedIncidentUrl: {
type: String,
required: false,
default: '',
},
issuableRef: {
type: String,
......@@ -380,7 +385,10 @@ export default {
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
<pinned-links :zoom-meeting-url="zoomMeetingUrl" />
<pinned-links
:zoom-meeting-url="zoomMeetingUrl"
:published-incident-url="publishedIncidentUrl"
/>
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
......
......@@ -11,21 +11,40 @@ export default {
zoomMeetingUrl: {
type: String,
required: false,
default: null,
default: '',
},
publishedIncidentUrl: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div v-if="zoomMeetingUrl" class="border-bottom mb-3 mt-n2">
<gl-link
:href="zoomMeetingUrl"
target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
>
<icon name="brand-zoom" :size="14" />
<strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
</gl-link>
<div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
<div v-if="publishedIncidentUrl" class="gl-pr-3">
<gl-link
:href="publishedIncidentUrl"
target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
data-testid="publishedIncidentUrl"
>
<icon name="tanuki" :size="14" />
<strong class="vertical-align-top">{{ __('Published on status page') }}</strong>
</gl-link>
</div>
<div v-if="zoomMeetingUrl">
<gl-link
:href="zoomMeetingUrl"
target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
data-testid="zoomMeetingUrl"
>
<icon name="brand-zoom" :size="14" />
<strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
</gl-link>
</div>
</div>
</template>
---
title: Add link to status page detail view for status page published issues
merge_request: 30249
author:
type: added
......@@ -90,6 +90,12 @@ After the quick action is used, a background worker publishes the issue onto the
Since all incidents are published publicly, user and group mentions are anonymized with `Incident Responder`,
and titles of non-public [GitLab references](../../markdown.md#special-gitlab-references) are removed.
When an Incident is published in the GitLab project, you can access the
details page of the Incident by clicking the **Published on status page** button
displayed under the Incident's title.
![Status Page detail link](../img/status_page_detail_link_v13_1.png)
NOTE: **Note:**
Confidential issues can't be published. If you make a published issue confidential, it will be unpublished.
......
......@@ -28,6 +28,15 @@ module EE
data
end
override :issue_only_initial_data
def issue_only_initial_data(issuable)
return {} unless issuable.is_a?(Issue)
super.merge(
publishedIncidentUrl: StatusPage::Storage.details_url(issuable)
)
end
override :issuable_meta_author_slot
def issuable_meta_author_slot(author, css_class: nil)
gitlab_team_member_badge(author, css_class: css_class)
......
......@@ -48,6 +48,17 @@ module StatusPage
super && project&.feature_available?(:status_page)
end
# Status page uses hash-routing, so we may see a number
# of different url endings from user-provided value;
# This ensures `/#/` is the tail
def normalized_status_page_url
return if status_page_url.blank?
status_page_url
.chomp('/').chomp('#').chomp('/')
.concat('/#/')
end
def storage_client
return unless enabled?
......
......@@ -13,22 +13,39 @@ module StatusPage
MAX_PAGES = 5
MAX_UPLOADS = MAX_KEYS_PER_PAGE * MAX_PAGES
def self.details_path(id)
"data/incident/#{id}.json"
end
class << self
def details_path(id)
"data/incident/#{id}.json"
end
def self.upload_path(issue_iid, secret, file_name)
uploads_path = self.uploads_path(issue_iid)
def details_url(issue)
return unless published_issue_available?(issue, issue.project.status_page_setting)
File.join(uploads_path, secret, file_name)
end
issue.project.status_page_setting.normalized_status_page_url +
CGI.escape(details_path(issue.iid))
end
def self.uploads_path(issue_iid)
File.join('data', 'incident', issue_iid.to_s, '/')
end
def upload_path(issue_iid, secret, file_name)
uploads_path = uploads_path(issue_iid)
File.join(uploads_path, secret, file_name)
end
def self.list_path
'data/list.json'
def uploads_path(issue_iid)
File.join('data', 'incident', issue_iid.to_s, '/')
end
def list_path
'data/list.json'
end
private
def published_issue_available?(issue, setting)
issue.status_page_published_incident &&
setting&.enabled? &&
setting&.status_page_url
end
end
class Error < StandardError
......
......@@ -44,11 +44,27 @@ RSpec.describe IssuablesHelper do
end
context 'for an issue' do
it 'returns the correct data that includes canAdmin: true' do
issue = create(:issue, author: user, description: 'issue text')
let_it_be(:issue) { create(:issue, author: user, description: 'issue text') }
it 'returns the correct data' do
@project = issue.project
expect(helper.issuable_initial_data(issue)).to include(canAdmin: true)
expected_data = {
canAdmin: true,
publishedIncidentUrl: nil
}
expect(helper.issuable_initial_data(issue)).to include(expected_data)
end
context 'when published to a configured status page' do
it 'returns the correct data that includes publishedIncidentUrl' do
@project = issue.project
expect(StatusPage::Storage).to receive(:details_url).with(issue).and_return('http://status.com')
expect(helper.issuable_initial_data(issue)).to include(
publishedIncidentUrl: 'http://status.com'
)
end
end
end
......
......@@ -9,6 +9,48 @@ RSpec.describe StatusPage::Storage do
it { is_expected.to eq('data/incident/123.json') }
end
describe '.details_url' do
let_it_be(:issue, reload: true) { create(:issue) }
subject { described_class.details_url(issue) }
context 'when issue is not published' do
it { is_expected.to be_nil }
end
context 'with a published incident' do
let_it_be(:incident) { create(:status_page_published_incident, issue: issue) }
context 'without a status page setting' do
it { is_expected.to be_nil }
end
context 'when status page setting is disabled' do
let_it_be(:setting) { create(:status_page_setting, project: issue.project) }
it { is_expected.to be_nil }
end
context 'when status page setting is enabled' do
let_it_be(:setting) { create(:status_page_setting, :enabled, project: issue.project) }
before do
stub_licensed_features(status_page: true)
end
it { is_expected.to eq("https://status.gitlab.com/#/data%2Fincident%2F#{issue.iid}.json") }
context 'when status page setting does not include a url' do
before do
setting.update!(status_page_url: nil)
end
it { is_expected.to be_nil }
end
end
end
end
describe '.list_path' do
subject { described_class.list_path }
......
......@@ -143,6 +143,48 @@ RSpec.describe StatusPage::ProjectSetting do
end
end
describe '#normalized_status_page_url' do
let(:status_page_setting) { build(:status_page_setting, status_page_url: status_page_url) }
let(:status_page_url) { 'https://status.gitlab.com' }
let(:expected_url) { 'https://status.gitlab.com/#/' }
subject { status_page_setting.normalized_status_page_url }
context 'when status_page_url exists' do
it { is_expected.to eq(expected_url) }
end
context 'when status_page_url is blank' do
let(:status_page_url) { '' }
it { is_expected.to be_nil }
end
context 'when status_page_url is nil' do
let(:status_page_url) { nil }
it { is_expected.to be_nil }
end
context 'when status_page_url contains trailing slash' do
let(:status_page_url) { 'https://status.gitlab.com/' }
it { is_expected.to eq(expected_url) }
end
context 'when status_page_url contains trailing hash-navigator' do
let(:status_page_url) { 'https://status.gitlab.com/#' }
it { is_expected.to eq(expected_url) }
end
context 'when status_page_url matches expected url' do
let(:status_page_url) { 'https://status.gitlab.com/#/' }
it { is_expected.to eq(expected_url) }
end
end
describe '#storage_client' do
let(:status_page_setting) { build(:status_page_setting, :enabled) }
......
......@@ -17965,6 +17965,9 @@ msgstr ""
msgid "Publish to status page"
msgstr ""
msgid "Published on status page"
msgstr ""
msgid "Publishes this issue to the associated status page."
msgstr ""
......
......@@ -17,6 +17,9 @@ jest.mock('~/issue_show/event_hub');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
const publishedIncidentUrl = 'https://status.com/';
describe('Issuable output', () => {
let mock;
let realtimeRequestCount = 0;
......@@ -67,6 +70,8 @@ describe('Issuable output', () => {
projectNamespace: '/',
projectPath: '/',
issuableTemplateNamesPath: '/issuable-templates-path',
zoomMeetingUrl,
publishedIncidentUrl,
},
}).$mount();
});
......@@ -132,7 +137,7 @@ describe('Issuable output', () => {
vm.canUpdate = false;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.btn')).toBeNull();
expect(vm.$el.querySelector('.markdown-selector')).toBeNull();
});
});
......@@ -183,6 +188,17 @@ describe('Issuable output', () => {
});
});
describe('Pinned links propagated', () => {
it.each`
prop | value
${'zoomMeetingUrl'} | ${zoomMeetingUrl}
${'publishedIncidentUrl'} | ${publishedIncidentUrl}
`('sets the $prop correctly on underlying pinned links', ({ prop, value }) => {
expect(vm[prop]).toEqual(value);
expect(vm.$el.querySelector(`[data-testid="${prop}"]`).href).toBe(value);
});
});
describe('updateIssuable', () => {
it('fetches new data after update', () => {
const updateStoreSpy = jest.spyOn(vm, 'updateStoreState');
......
......@@ -3,23 +3,18 @@ import { GlLink } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
const plainZoomUrl = 'https://zoom.us/j/123456789';
const plainStatusUrl = 'https://status.com';
describe('PinnedLinks', () => {
let wrapper;
const link = {
get text() {
return wrapper.find(GlLink).text();
},
get href() {
return wrapper.find(GlLink).attributes('href');
},
};
const findLinks = () => wrapper.findAll(GlLink);
const createComponent = props => {
wrapper = shallowMount(PinnedLinks, {
propsData: {
zoomMeetingUrl: null,
zoomMeetingUrl: '',
publishedIncidentUrl: '',
...props,
},
});
......@@ -30,12 +25,29 @@ describe('PinnedLinks', () => {
zoomMeetingUrl: `<a href="${plainZoomUrl}">Zoom</a>`,
});
expect(link.text).toBe('Join Zoom meeting');
expect(
findLinks()
.at(0)
.text(),
).toBe('Join Zoom meeting');
});
it('displays Status link', () => {
createComponent({
publishedIncidentUrl: `<a href="${plainStatusUrl}">Status</a>`,
});
expect(
findLinks()
.at(0)
.text(),
).toBe('Published on status page');
});
it('does not render if there are no links', () => {
createComponent({
zoomMeetingUrl: null,
zoomMeetingUrl: '',
publishedIncidentUrl: '',
});
expect(wrapper.find(GlLink).exists()).toBe(false);
......
......@@ -6,6 +6,15 @@ import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data
describe('RelatedIssuableItem', () => {
let wrapper;
function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) {
wrapper = mountMethod(RelatedIssuableItem, {
propsData: props,
slots,
stubs,
});
}
const props = {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
......@@ -26,10 +35,7 @@ describe('RelatedIssuableItem', () => {
};
beforeEach(() => {
wrapper = mount(RelatedIssuableItem, {
slots,
propsData: props,
});
mountComponent({ props, slots });
});
afterEach(() => {
......
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