Commit c67b571a authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '336624-restrict-permissions-to-create-an-incident-to-reporter-and-up' into 'master'

Restrict permissions to create an incident to Reporter and up

See merge request gitlab-org/gitlab!69350
parents edd23d67 280ff64e
...@@ -125,6 +125,7 @@ export default { ...@@ -125,6 +125,7 @@ export default {
'authorUsernameQuery', 'authorUsernameQuery',
'assigneeUsernameQuery', 'assigneeUsernameQuery',
'slaFeatureAvailable', 'slaFeatureAvailable',
'canCreateIncident',
], ],
apollo: { apollo: {
incidents: { incidents: {
...@@ -230,13 +231,16 @@ export default { ...@@ -230,13 +231,16 @@ export default {
}, },
emptyStateData() { emptyStateData() {
const { const {
emptyState: { title, emptyClosedTabTitle, description }, emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
createIncidentBtnLabel, createIncidentBtnLabel,
} = this.$options.i18n; } = this.$options.i18n;
if (this.activeClosedTabHasNoIncidents) { if (this.activeClosedTabHasNoIncidents) {
return { title: emptyClosedTabTitle }; return { title: emptyClosedTabTitle };
} }
if (!this.canCreateIncident) {
return { title, description: cannotCreateIncidentDescription };
}
return { return {
title, title,
description, description,
...@@ -244,6 +248,9 @@ export default { ...@@ -244,6 +248,9 @@ export default {
btnText: createIncidentBtnLabel, btnText: createIncidentBtnLabel,
}; };
}, },
isHeaderButtonVisible() {
return this.canCreateIncident && (!this.isEmpty || this.activeClosedTabHasNoIncidents);
},
}, },
methods: { methods: {
hasAssignees(assignees) { hasAssignees(assignees) {
...@@ -311,7 +318,7 @@ export default { ...@@ -311,7 +318,7 @@ export default {
> >
<template #header-actions> <template #header-actions>
<gl-button <gl-button
v-if="!isEmpty || activeClosedTabHasNoIncidents" v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button" class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn" data-testid="createIncidentBtn"
data-qa-selector="create_incident_button" data-qa-selector="create_incident_button"
......
...@@ -11,7 +11,10 @@ export const I18N = { ...@@ -11,7 +11,10 @@ export const I18N = {
title: s__('IncidentManagement|Display your incidents in a dedicated view'), title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'), emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__( description: s__(
'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.', 'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below.',
),
cannotCreateIncidentDescription: s__(
'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list.',
), ),
}, },
}; };
......
...@@ -21,6 +21,7 @@ export default () => { ...@@ -21,6 +21,7 @@ export default () => {
authorUsernameQuery, authorUsernameQuery,
assigneeUsernameQuery, assigneeUsernameQuery,
slaFeatureAvailable, slaFeatureAvailable,
canCreateIncident,
} = domEl.dataset; } = domEl.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
...@@ -44,6 +45,7 @@ export default () => { ...@@ -44,6 +45,7 @@ export default () => {
authorUsernameQuery, authorUsernameQuery,
assigneeUsernameQuery, assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable), slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
canCreateIncident: parseBoolean(canCreateIncident),
}, },
apolloProvider, apolloProvider,
render(createElement) { render(createElement) {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash'; import { capitalize } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { IssuableTypes } from '../../constants'; import { IssuableTypes, IncidentType } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql'; import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql'; import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
...@@ -19,6 +19,14 @@ export default { ...@@ -19,6 +19,14 @@ export default {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
}, },
inject: {
canCreateIncident: {
default: false,
},
issueType: {
default: 'issue',
},
},
data() { data() {
return { return {
issueState: {}, issueState: {},
...@@ -36,6 +44,9 @@ export default { ...@@ -36,6 +44,9 @@ export default {
} = this; } = this;
return capitalize(issueType); return capitalize(issueType);
}, },
shouldShowIncident() {
return this.issueType === IncidentType || this.canCreateIncident;
},
}, },
methods: { methods: {
updateIssueType(issueType) { updateIssueType(issueType) {
...@@ -47,6 +58,9 @@ export default { ...@@ -47,6 +58,9 @@ export default {
}, },
}); });
}, },
isShown(type) {
return type.value !== IncidentType || this.shouldShowIncident;
},
}, },
}; };
</script> </script>
...@@ -68,6 +82,7 @@ export default { ...@@ -68,6 +82,7 @@ export default {
> >
<gl-dropdown-item <gl-dropdown-item
v-for="type in $options.IssuableTypes" v-for="type in $options.IssuableTypes"
v-show="isShown(type)"
:key="type.value" :key="type.value"
:is-checked="issueState.issueType === type.value" :is-checked="issueState.issueType === type.value"
is-check-item is-check-item
......
...@@ -2,25 +2,31 @@ import Vue from 'vue'; ...@@ -2,25 +2,31 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue'; import incidentTabs from './components/incidents/incident_tabs.vue';
import { issueState } from './constants'; import { issueState, IncidentType } from './constants';
import apolloProvider from './graphql'; import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql'; import getIssueStateQuery from './queries/get_issue_state.query.graphql';
import HeaderActions from './components/header_actions.vue';
export default function initIssuableApp(issuableData = {}) { const bootstrapApollo = (state = {}) => {
return apolloProvider.clients.defaultClient.cache.writeQuery({
query: getIssueStateQuery,
data: {
issueState: state,
},
});
};
export function initIncidentApp(issuableData = {}) {
const el = document.getElementById('js-issuable-app'); const el = document.getElementById('js-issuable-app');
if (!el) { if (!el) {
return undefined; return undefined;
} }
apolloProvider.clients.defaultClient.cache.writeQuery({ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
query: getIssueStateQuery,
data: {
issueState: { ...issueState, issueType: el.dataset.issueType },
},
});
const { const {
canCreateIncident,
canUpdate, canUpdate,
iid, iid,
projectNamespace, projectNamespace,
...@@ -39,6 +45,8 @@ export default function initIssuableApp(issuableData = {}) { ...@@ -39,6 +45,8 @@ export default function initIssuableApp(issuableData = {}) {
issuableApp, issuableApp,
}, },
provide: { provide: {
issueType: IncidentType,
canCreateIncident,
canUpdate, canUpdate,
fullPath, fullPath,
iid, iid,
...@@ -57,3 +65,35 @@ export default function initIssuableApp(issuableData = {}) { ...@@ -57,3 +65,35 @@ export default function initIssuableApp(issuableData = {}) {
}, },
}); });
} }
export function initIncidentHeaderActions(store) {
const el = document.querySelector('.js-issue-header-actions');
if (!el) {
return undefined;
}
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
return new Vue({
el,
apolloProvider,
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
render: (createElement) => createElement(HeaderActions),
});
}
...@@ -25,17 +25,22 @@ export function initIssuableApp(issuableData, store) { ...@@ -25,17 +25,22 @@ export function initIssuableApp(issuableData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType }); bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const { canCreateIncident, ...issuableProps } = issuableData;
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
store, store,
provide: {
canCreateIncident,
},
computed: { computed: {
...mapGetters(['getNoteableData']), ...mapGetters(['getNoteableData']),
}, },
render(createElement) { render(createElement) {
return createElement(IssuableApp, { return createElement(IssuableApp, {
props: { props: {
...issuableData, ...issuableProps,
isConfidential: this.getNoteableData?.confidential, isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked, isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state, issuableStatus: this.getNoteableData?.state,
......
...@@ -3,7 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -3,7 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants'; import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue'; import Issue from '~/issue';
import initIncidentApp from '~/issue_show/incident'; import { initIncidentApp, initIncidentHeaderActions } from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue'; import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data'; import { parseIssuableData } from '~/issue_show/utils/parse_data';
import initNotesApp from '~/notes'; import initNotesApp from '~/notes';
...@@ -22,16 +22,18 @@ export default function initShowIssue() { ...@@ -22,16 +22,18 @@ export default function initShowIssue() {
switch (issueType) { switch (issueType) {
case IssuableType.Incident: case IssuableType.Incident:
initIncidentApp(issuableData); initIncidentApp(issuableData);
initIncidentHeaderActions(store);
break; break;
case IssuableType.Issue: case IssuableType.Issue:
initIssuableApp(issuableData, store); initIssuableApp(issuableData, store);
initIssueHeaderActions(store);
break; break;
default: default:
initIssueHeaderActions(store);
break; break;
} }
initIssuableHeaderWarning(store); initIssuableHeaderWarning(store);
initIssueHeaderActions(store);
initSentryErrorStackTraceApp(); initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp(); initRelatedMergeRequestsApp();
......
...@@ -257,7 +257,8 @@ module IssuablesHelper ...@@ -257,7 +257,8 @@ module IssuablesHelper
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable), zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
iid: issuable.iid.to_s, iid: issuable.iid.to_s,
isHidden: issue_hidden?(issuable) isHidden: issue_hidden?(issuable),
canCreateIncident: create_issue_type_allowed?(issuable.project, :incident)
} }
end end
......
...@@ -192,6 +192,7 @@ module IssuesHelper ...@@ -192,6 +192,7 @@ module IssuesHelper
{ {
can_create_issue: show_new_issue_link?(project).to_s, can_create_issue: show_new_issue_link?(project).to_s,
can_create_incident: create_issue_type_allowed?(project, :incident).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s, can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s,
can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s, can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issuable).to_s, can_update_issue: can?(current_user, :update_issue, issuable).to_s,
......
...@@ -11,7 +11,8 @@ module Projects::IncidentsHelper ...@@ -11,7 +11,8 @@ module Projects::IncidentsHelper
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'), 'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
'text-query': params[:search], 'text-query': params[:search],
'author-username-query': params[:author_username], 'author-username-query': params[:author_username],
'assignee-username-query': params[:assignee_username] 'assignee-username-query': params[:assignee_username],
'can-create-incident': create_issue_type_allowed?(project, :incident).to_s
} }
end end
end end
......
...@@ -248,7 +248,7 @@ class ProjectPolicy < BasePolicy ...@@ -248,7 +248,7 @@ class ProjectPolicy < BasePolicy
enable :read_insights enable :read_insights
end end
rule { can?(:guest_access) & can?(:create_issue) }.enable :create_incident rule { can?(:reporter_access) & can?(:create_issue) }.enable :create_incident
# These abilities are not allowed to admins that are not members of the project, # These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separately. # that's why they are defined separately.
......
...@@ -19,20 +19,22 @@ You can create an incident manually or automatically. ...@@ -19,20 +19,22 @@ You can create an incident manually or automatically.
### Create incidents manually ### Create incidents manually
If you have at least Guest [permissions](../../user/permissions.md), to create an > [Permission changed](https://gitlab.com/gitlab-org/gitlab/-/issues/336624) from Guest to Reporter in GitLab 14.5.
Incident, you have two options to do this manually.
**From the Incidents List:** If you have at least Reporter [permissions](../../user/permissions.md),
you can create an incident manually from the Incidents List or the Issues List.
To create an incident from the Incidents List:
> [Moved](https://gitlab.com/gitlab-org/monitor/monitor/-/issues/24) to GitLab Free in 13.3. > [Moved](https://gitlab.com/gitlab-org/monitor/monitor/-/issues/24) to GitLab Free in 13.3.
- Navigate to **Monitor > Incidents** and click **Create Incident**. 1. Navigate to **Monitor > Incidents** and click **Create Incident**.
- Create a new issue using the `incident` template available when creating it. 1. Create a new issue using the `incident` template available when creating it.
- Create a new issue and assign the `incident` label to it. 1. Create a new issue and assign the `incident` label to it.
![Incident List Create](img/incident_list_create_v13_3.png) ![Incident List Create](img/incident_list_create_v13_3.png)
**From the Issues List:** To create an incident from the Issues List:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230857) in GitLab 13.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230857) in GitLab 13.4.
......
...@@ -84,7 +84,7 @@ The following table lists project permissions available for each role: ...@@ -84,7 +84,7 @@ The following table lists project permissions available for each role:
| [Incident Management](../operations/incident_management/index.md):<br>View [alerts](../operations/incident_management/alerts.md) | | ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>View [alerts](../operations/incident_management/alerts.md) | | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Assign an alert | ✓| ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>Assign an alert | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [incident](../operations/incident_management/incidents.md) | ✓| ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>View [incident](../operations/incident_management/incidents.md) | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Create [incident](../operations/incident_management/incidents.md) | | ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>Create [incident](../operations/incident_management/incidents.md) | (*17*) | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [on-call schedules](../operations/incident_management/oncall_schedules.md) | | ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>View [on-call schedules](../operations/incident_management/oncall_schedules.md) | | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Participate in on-call rotation | ✓| ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>Participate in on-call rotation | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [escalation policies](../operations/incident_management/escalation_policies.md) | | ✓ | ✓ | ✓ | ✓ | | [Incident Management](../operations/incident_management/index.md):<br>View [escalation policies](../operations/incident_management/escalation_policies.md) | | ✓ | ✓ | ✓ | ✓ |
...@@ -226,6 +226,7 @@ The following table lists project permissions available for each role: ...@@ -226,6 +226,7 @@ The following table lists project permissions available for each role:
1. Attached design files are moved together with the issue even if the user doesn't have the 1. Attached design files are moved together with the issue even if the user doesn't have the
Developer role. Developer role.
1. Guest users can set metadata (for example, labels, assignees, or milestones) when creating an issue. 1. Guest users can set metadata (for example, labels, assignees, or milestones) when creating an issue.
1. In GitLab 14.5 or later, Guests are not allowed to [create incidents](../operations/incident_management/incidents.md#incident-creation).
## Project features permissions ## Project features permissions
......
...@@ -15,6 +15,7 @@ const defaultProvide = { ...@@ -15,6 +15,7 @@ const defaultProvide = {
authorUsernameQuery: '', authorUsernameQuery: '',
assigneeUsernameQuery: '', assigneeUsernameQuery: '',
slaFeatureAvailable: true, slaFeatureAvailable: true,
canCreateIncident: true,
}; };
describe('Incidents Service Level Agreement', () => { describe('Incidents Service Level Agreement', () => {
......
...@@ -5,8 +5,8 @@ require 'spec_helper' ...@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
let_it_be_with_refind(:project) { create(:project) } let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) } let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) } let(:issue_path) { project_issues_path(project) }
...@@ -18,6 +18,13 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -18,6 +18,13 @@ RSpec.describe Projects::IncidentsHelper do
} }
end end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?)
.with(user, :create_incident, project)
.and_return(true)
end
describe '#incidents_data' do describe '#incidents_data' do
let(:expected_incidents_data) do let(:expected_incidents_data) do
{ {
...@@ -31,7 +38,8 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -31,7 +38,8 @@ RSpec.describe Projects::IncidentsHelper do
'sla-feature-available' => 'false', 'sla-feature-available' => 'false',
'text-query': 'search text', 'text-query': 'search text',
'author-username-query': 'root', 'author-username-query': 'root',
'assignee-username-query': 'max.power' 'assignee-username-query': 'max.power',
'can-create-incident': 'true'
} }
end end
......
...@@ -18101,7 +18101,10 @@ msgstr "" ...@@ -18101,7 +18101,10 @@ msgstr ""
msgid "IncidentManagement|All" msgid "IncidentManagement|All"
msgstr "" msgstr ""
msgid "IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below." msgid "IncidentManagement|All alerts promoted to incidents are automatically displayed within the list."
msgstr ""
msgid "IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below."
msgstr "" msgstr ""
msgid "IncidentManagement|Assignees" msgid "IncidentManagement|Assignees"
......
...@@ -4,52 +4,49 @@ require 'spec_helper' ...@@ -4,52 +4,49 @@ require 'spec_helper'
RSpec.describe 'Incident Management index', :js do RSpec.describe 'Incident Management index', :js do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) } let_it_be(:incident) { create(:incident, project: project) }
before_all do before_all do
project.add_developer(developer) project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
end end
shared_examples 'create incident form' do before do
it 'shows the create new issue button' do sign_in(user)
expect(page).to have_selector('.create-incident-button')
end
it 'when clicked shows the create issue page with the Incident type pre-selected' do visit project_incidents_path(project)
find('.create-incident-button').click wait_for_all_requests
wait_for_all_requests end
expect(page).to have_selector('.dropdown-menu-toggle') describe 'incident list is visited' do
expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap') context 'by reporter' do
let(:user) { reporter }
page.within('.js-issuable-type-filter-dropdown-wrap') do it 'shows the create new incident button' do
expect(page).to have_content('Incident') expect(page).to have_selector('.create-incident-button')
end end
end
end
context 'when a developer displays the incident list' do it 'when clicked shows the create issue page with the Incident type pre-selected' do
before do find('.create-incident-button').click
sign_in(developer) wait_for_all_requests
visit project_incidents_path(project) expect(page).to have_selector('.dropdown-menu-toggle')
wait_for_all_requests expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
end
it_behaves_like 'create incident form' page.within('.js-issuable-type-filter-dropdown-wrap') do
expect(page).to have_content('Incident')
end
end
end
end end
context 'when a guest displays the incident list' do context 'by guest' do
before do let(:user) { guest }
sign_in(guest)
visit project_incidents_path(project) it 'does not show new incident button' do
wait_for_all_requests expect(page).not_to have_selector('.create-incident-button')
end end
it_behaves_like 'create incident form'
end end
end end
...@@ -22,12 +22,30 @@ RSpec.describe "User views incident" do ...@@ -22,12 +22,30 @@ RSpec.describe "User views incident" do
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet' it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
it 'shows the merge request and incident actions', :js, :aggregate_failures do describe 'user actions' do
click_button 'Incident actions' it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } })) expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_button('Create merge request') expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident') expect(page).to have_button('Close incident')
end
context 'when user is a guest' do
before do
project.add_guest(user)
login_as(user)
visit(project_issues_incident_path(project, incident))
end
it 'does not show the incident action', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).not_to have_link('New incident')
end
end
end end
context 'when the project is archived' do context 'when the project is archived' do
......
...@@ -248,15 +248,21 @@ RSpec.describe 'New/edit issue', :js do ...@@ -248,15 +248,21 @@ RSpec.describe 'New/edit issue', :js do
end end
end end
shared_examples 'type option is missing' do |label:, identifier:|
it "does not show #{identifier} option", :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).not_to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
expect(page).not_to have_content(label)
end
end
end
before do before do
page.within('.issue-form') do page.within('.issue-form') do
click_button 'Issue' click_button 'Issue'
end end
end end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
context 'when user is guest' do context 'when user is guest' do
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
...@@ -266,6 +272,19 @@ RSpec.describe 'New/edit issue', :js do ...@@ -266,6 +272,19 @@ RSpec.describe 'New/edit issue', :js do
project.add_guest(guest) project.add_guest(guest)
end end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is missing', label: 'Incident', identifier: :incident
end
context 'when user is reporter' do
let_it_be(:reporter) { create(:user) }
let(:current_user) { reporter }
before_all do
project.add_reporter(reporter)
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
end end
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Issue Detail', :js do RSpec.describe 'Issue Detail', :js do
let_it_be_with_refind(:project) { create(:project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) } let(:issue) { create(:issue, project: project, author: user) }
let(:incident) { create(:incident, project: project, author: user) } let(:incident) { create(:incident, project: project, author: user) }
...@@ -90,7 +91,13 @@ RSpec.describe 'Issue Detail', :js do ...@@ -90,7 +91,13 @@ RSpec.describe 'Issue Detail', :js do
end end
describe 'user updates `issue_type` via the issue type dropdown' do describe 'user updates `issue_type` via the issue type dropdown' do
context 'when an issue `issue_type` is edited by a signed in user' do let_it_be(:reporter) { create(:user) }
before_all do
project.add_reporter(reporter)
end
describe 'when an issue `issue_type` is edited' do
before do before do
sign_in(user) sign_in(user)
...@@ -98,18 +105,33 @@ RSpec.describe 'Issue Detail', :js do ...@@ -98,18 +105,33 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests wait_for_requests
end end
it 'routes the user to the incident details page when the `issue_type` is set to incident' do context 'by non-member author' do
open_issue_edit_form it 'cannot see Incident option' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do
expect(page).to have_content('Issue')
expect(page).not_to have_content('Incident')
end
end
end
context 'by reporter' do
let(:user) { reporter }
page.within('[data-testid="issuable-form"]') do it 'routes the user to the incident details page when the `issue_type` is set to incident' do
update_type_select('Issue', 'Incident') open_issue_edit_form
expect(page).to have_current_path(project_issues_incident_path(project, issue)) page.within('[data-testid="issuable-form"]') do
update_type_select('Issue', 'Incident')
expect(page).to have_current_path(project_issues_incident_path(project, issue))
end
end end
end end
end end
context 'when an incident `issue_type` is edited by a signed in user' do describe 'when an incident `issue_type` is edited' do
before do before do
sign_in(user) sign_in(user)
...@@ -117,13 +139,29 @@ RSpec.describe 'Issue Detail', :js do ...@@ -117,13 +139,29 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests wait_for_requests
end end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do context 'by non-member author' do
open_issue_edit_form it 'routes the user to the issue details page when the `issue_type` is set to issue' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do
update_type_select('Incident', 'Issue')
expect(page).to have_current_path(project_issue_path(project, incident))
end
end
end
context 'by reporter' do
let(:user) { reporter }
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do page.within('[data-testid="issuable-form"]') do
update_type_select('Incident', 'Issue') update_type_select('Incident', 'Issue')
expect(page).to have_current_path(project_issue_path(project, incident)) expect(page).to have_current_path(project_issue_path(project, incident))
end
end end
end end
end end
......
...@@ -171,7 +171,7 @@ RSpec.describe "User creates issue" do ...@@ -171,7 +171,7 @@ RSpec.describe "User creates issue" do
end end
context 'form create handles issue creation by default' do context 'form create handles issue creation by default' do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
before do before do
visit new_project_issue_path(project) visit new_project_issue_path(project)
...@@ -187,30 +187,22 @@ RSpec.describe "User creates issue" do ...@@ -187,30 +187,22 @@ RSpec.describe "User creates issue" do
end end
context 'form create handles incident creation' do context 'form create handles incident creation' do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
before do before do
visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }) visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
end end
it 'pre-fills the issue type dropdown with incident type' do it 'does not pre-fill the issue type dropdown with incident type' do
expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident') expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).not_to have_content('Incident')
end
it 'hides the epic select' do
expect(page).not_to have_selector('.epic-dropdown-container')
end end
it 'shows the milestone select' do it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
end end
it 'hides the weight input' do it 'hides the incident help text' do
expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage expect(page).not_to have_text('A modified issue to guide the resolution of incidents.')
end
it 'shows the incident help text' do
expect(page).to have_text('A modified issue to guide the resolution of incidents.')
end end
end end
...@@ -242,6 +234,44 @@ RSpec.describe "User creates issue" do ...@@ -242,6 +234,44 @@ RSpec.describe "User creates issue" do
end end
end end
context 'when signed in as reporter', :js do
let_it_be(:project) { create(:project) }
before_all do
project.add_reporter(user)
end
before do
sign_in(user)
end
context 'form create handles incident creation' do
before do
visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
end
it 'pre-fills the issue type dropdown with incident type' do
expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident')
end
it 'hides the epic select' do
expect(page).not_to have_selector('.epic-dropdown-container')
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
end
it 'hides the weight input' do
expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
end
it 'shows the incident help text' do
expect(page).to have_text('A modified issue to guide the resolution of incidents.')
end
end
end
context "when signed in as user with special characters in their name" do context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") } let(:user_special) { create(:user, name: "Jon O'Shea") }
......
...@@ -78,6 +78,7 @@ describe('Incidents List', () => { ...@@ -78,6 +78,7 @@ describe('Incidents List', () => {
authorUsernameQuery: '', authorUsernameQuery: '',
assigneeUsernameQuery: '', assigneeUsernameQuery: '',
slaFeatureAvailable: true, slaFeatureAvailable: true,
canCreateIncident: true,
...provide, ...provide,
}, },
stubs: { stubs: {
...@@ -105,21 +106,23 @@ describe('Incidents List', () => { ...@@ -105,21 +106,23 @@ describe('Incidents List', () => {
describe('empty state', () => { describe('empty state', () => {
const { const {
emptyState: { title, emptyClosedTabTitle, description }, emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
} = I18N; } = I18N;
it.each` it.each`
statusFilter | all | closed | expectedTitle | expectedDescription statusFilter | all | closed | expectedTitle | canCreateIncident | expectedDescription
${'all'} | ${2} | ${1} | ${title} | ${description} ${'all'} | ${2} | ${1} | ${title} | ${true} | ${description}
${'open'} | ${2} | ${0} | ${title} | ${description} ${'open'} | ${2} | ${0} | ${title} | ${true} | ${description}
${'closed'} | ${0} | ${0} | ${title} | ${description} ${'closed'} | ${0} | ${0} | ${title} | ${true} | ${description}
${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${undefined} ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${true} | ${undefined}
${'all'} | ${2} | ${1} | ${title} | ${false} | ${cannotCreateIncidentDescription}
`( `(
`when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state `when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state
has title: $expectedTitle and description: $expectedDescription`, has title: $expectedTitle and description: $expectedDescription`,
({ statusFilter, all, closed, expectedTitle, expectedDescription }) => { ({ statusFilter, all, closed, expectedTitle, expectedDescription, canCreateIncident }) => {
mountComponent({ mountComponent({
data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter }, data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter },
provide: { canCreateIncident },
loading: false, loading: false,
}); });
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
...@@ -219,6 +222,15 @@ describe('Incidents List', () => { ...@@ -219,6 +222,15 @@ describe('Incidents List', () => {
expect(findCreateIncidentBtn().exists()).toBe(false); expect(findCreateIncidentBtn().exists()).toBe(false);
}); });
it("doesn't show the button when user does not have incident creation permissions", () => {
mountComponent({
data: { incidents: { list: mockIncidents }, incidentsCount: {} },
provide: { canCreateIncident: false },
loading: false,
});
expect(findCreateIncidentBtn().exists()).toBe(false);
});
it('should track create new incident button', async () => { it('should track create new incident button', async () => {
findCreateIncidentBtn().vm.$emit('click'); findCreateIncidentBtn().vm.$emit('click');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
......
...@@ -39,7 +39,7 @@ describe('Issue type field component', () => { ...@@ -39,7 +39,7 @@ describe('Issue type field component', () => {
const findTypeFromDropDownItemIconAt = (at) => const findTypeFromDropDownItemIconAt = (at) =>
findTypeFromDropDownItems().at(at).findComponent(GlIcon); findTypeFromDropDownItems().at(at).findComponent(GlIcon);
const createComponent = ({ data } = {}) => { const createComponent = ({ data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers); fakeApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(IssueTypeField, { wrapper = shallowMount(IssueTypeField, {
...@@ -51,6 +51,10 @@ describe('Issue type field component', () => { ...@@ -51,6 +51,10 @@ describe('Issue type field component', () => {
...data, ...data,
}; };
}, },
provide: {
canCreateIncident: true,
...provide,
},
}); });
}; };
...@@ -92,5 +96,25 @@ describe('Issue type field component', () => { ...@@ -92,5 +96,25 @@ describe('Issue type field component', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident); expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
}); });
describe('when user is a guest', () => {
it('hides the incident type from the dropdown', async () => {
createComponent({}, { canCreateIncident: false, issueType: 'issue' });
await waitForPromises();
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
createComponent({}, { canCreateIncident: false, issueType: 'incident' });
await waitForPromises();
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
});
});
}); });
}); });
...@@ -5,7 +5,8 @@ require 'spec_helper' ...@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
let(:project) { create(:project) } let(:user) { build_stubbed(:user) }
let(:project) { build_stubbed(:project) }
let(:project_path) { project.full_path } let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) } let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) } let(:issue_path) { project_issues_path(project) }
...@@ -17,21 +18,43 @@ RSpec.describe Projects::IncidentsHelper do ...@@ -17,21 +18,43 @@ RSpec.describe Projects::IncidentsHelper do
} }
end end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?)
.with(user, :create_incident, project)
.and_return(can_create_incident)
end
describe '#incidents_data' do describe '#incidents_data' do
subject(:data) { helper.incidents_data(project, params) } subject(:data) { helper.incidents_data(project, params) }
it 'returns frontend configuration' do shared_examples 'frontend configuration' do
expect(data).to include( it 'returns frontend configuration' do
'project-path' => project_path, expect(data).to include(
'new-issue-path' => new_issue_path, 'project-path' => project_path,
'incident-template-name' => 'incident', 'new-issue-path' => new_issue_path,
'incident-type' => 'incident', 'incident-template-name' => 'incident',
'issue-path' => issue_path, 'incident-type' => 'incident',
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'), 'issue-path' => issue_path,
'text-query': 'search text', 'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
'author-username-query': 'root', 'text-query': 'search text',
'assignee-username-query': 'max.power' 'author-username-query': 'root',
) 'assignee-username-query': 'max.power',
'can-create-incident': can_create_incident.to_s
)
end
end
context 'when user can create incidents' do
let(:can_create_incident) { true }
include_examples 'frontend configuration'
end
context 'when user cannot create incidents' do
let(:can_create_incident) { false }
include_examples 'frontend configuration'
end end
end end
end end
...@@ -7,12 +7,14 @@ RSpec.describe Issues::BuildService do ...@@ -7,12 +7,14 @@ RSpec.describe Issues::BuildService do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let(:user) { developer } let(:user) { developer }
before_all do before_all do
project.add_developer(developer) project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
end end
...@@ -177,7 +179,8 @@ RSpec.describe Issues::BuildService do ...@@ -177,7 +179,8 @@ RSpec.describe Issues::BuildService do
where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
nil | ref(:guest) | ref(:type_issue_id) | 'issue' nil | ref(:guest) | ref(:type_issue_id) | 'issue'
'issue' | ref(:guest) | ref(:type_issue_id) | 'issue' 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
'incident' | ref(:guest) | ref(:type_incident_id) | 'incident' 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue'
'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident'
# update once support for test_case is enabled # update once support for test_case is enabled
'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue' 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
# update once support for requirement is enabled # update once support for requirement is enabled
......
...@@ -80,7 +80,7 @@ RSpec.describe Issues::CreateService do ...@@ -80,7 +80,7 @@ RSpec.describe Issues::CreateService do
it_behaves_like 'not an incident issue' it_behaves_like 'not an incident issue'
context 'issue is incident type' do context 'when issue is incident type' do
before do before do
opts.merge!(issue_type: 'incident') opts.merge!(issue_type: 'incident')
end end
...@@ -90,23 +90,37 @@ RSpec.describe Issues::CreateService do ...@@ -90,23 +90,37 @@ RSpec.describe Issues::CreateService do
subject { issue } subject { issue }
it_behaves_like 'incident issue' context 'as reporter' do
it_behaves_like 'has incident label' let_it_be(:reporter) { create(:user) }
it 'does create an incident label' do let(:user) { reporter }
expect { subject }
.to change { Label.where(incident_label_attributes).count }.by(1)
end
context 'when invalid' do before_all do
before do project.add_reporter(reporter)
opts.merge!(title: '') end
it_behaves_like 'incident issue'
it_behaves_like 'has incident label'
it 'does create an incident label' do
expect { subject }
.to change { Label.where(incident_label_attributes).count }.by(1)
end end
it 'does not apply an incident label prematurely' do context 'when invalid' do
expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count) before do
opts.merge!(title: '')
end
it 'does not apply an incident label prematurely' do
expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
end
end end
end end
context 'as guest' do
it_behaves_like 'not an incident issue'
end
end end
it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
......
...@@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do ...@@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_guest_permissions) do let(:base_guest_permissions) do
%i[ %i[
award_emoji create_issue create_incident create_merge_request_in create_note award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_issue_board_list read_milestone read_note read_project read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet read_project_for_iids read_project_member read_release read_snippet
...@@ -25,10 +25,11 @@ RSpec.shared_context 'ProjectPolicy context' do ...@@ -25,10 +25,11 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do let(:base_reporter_permissions) do
%i[ %i[
admin_issue admin_issue_link admin_label admin_issue_board_list create_snippet admin_issue admin_issue_link admin_label admin_issue_board_list
daily_statistics download_code download_wiki_code fork_project metrics_dashboard create_snippet create_incident daily_statistics download_code
read_build read_commit_status read_confidential_issues download_wiki_code fork_project metrics_dashboard read_build
read_container_image read_deployment read_environment read_merge_request read_commit_status read_confidential_issues read_container_image
read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus read_metrics_dashboard_annotation read_pipeline read_prometheus
read_sentry_issue update_issue read_sentry_issue update_issue
] ]
......
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