Commit e5d7d61b authored by Denys Mishunov's avatar Denys Mishunov

Merge branch 'sy-publish-status-ui' into 'master'

Expose url of published incident on status page on issue

See merge request gitlab-org/gitlab!30249
parents 8ab77787 b5e1b76e
......@@ -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) }
......
......@@ -18016,6 +18016,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