Commit 944a517e authored by Tristan Read's avatar Tristan Read Committed by Peter Leitzen

Add on call scheduling

Revert line removal

Add FE component

Move OncallSchedulesController into EE

Move OncallSchedulesController and its related files to ee namespace

Add empty state on FE side

Change on-call schedules path to /-/oncall-schedules

Add on-call schedules sidebar link

Check permissions for OncallSchedulesController

Add OncallSchedulesController to feature categories

Address reviewer feedback

Address BE code review feedback

Revert testing for translations

Update translation string

Use helper method to check for nav item

Fix failing spec

Move oncall schedules nav item to ee folder

Move on-call schedule license check to policy

Move on-call schedule feature flag and licence check into policies

No need to check EE ability in FOSS code

Check the ability of viewing the operations tab in EE only

Check sidebar operations path in EE-only
parent 39f7bc82
...@@ -467,9 +467,7 @@ module ProjectsHelper ...@@ -467,9 +467,7 @@ module ProjectsHelper
} }
end end
def can_view_operations_tab?(current_user, project) def view_operations_tab_ability
return false unless project.feature_available?(:operations, current_user)
[ [
:metrics_dashboard, :metrics_dashboard,
:read_alert_management_alert, :read_alert_management_alert,
...@@ -479,7 +477,13 @@ module ProjectsHelper ...@@ -479,7 +477,13 @@ module ProjectsHelper
:read_cluster, :read_cluster,
:read_feature_flag, :read_feature_flag,
:read_terraform_state :read_terraform_state
].any? do |ability| ]
end
def can_view_operations_tab?(current_user, project)
return false unless project.feature_available?(:operations, current_user)
view_operations_tab_ability.any? do |ability|
can?(current_user, ability, project) can?(current_user, ability, project)
end end
end end
......
...@@ -262,6 +262,8 @@ ...@@ -262,6 +262,8 @@
%span %span
= _('Incidents') = _('Incidents')
= render_if_exists 'projects/sidebar/oncall_schedules'
- if project_nav_tab? :serverless - if project_nav_tab? :serverless
= nav_link(controller: :functions) do = nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do = link_to project_serverless_functions_path(@project), title: _('Serverless') do
......
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export const i18n = {
emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
description: s__('OnCallSchedules|Route alerts directly to specific members of your team'),
button: s__('OnCallSchedules|Add a schedule'),
},
};
export default {
i18n,
inject: ['emptyOncallSchedulesSvgPath'],
components: {
GlEmptyState,
GlButton,
},
methods: {
createSchedule() {},
},
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:svg-path="emptyOncallSchedulesSvgPath"
>
<template #actions>
<gl-button variant="info" @click="createSchedule">{{
$options.i18n.emptyState.button
}}</gl-button>
</template>
</gl-empty-state>
</template>
import Vue from 'vue';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
export default () => {
const el = document.querySelector('#js-oncall_schedule');
if (!el) return null;
const { emptyOncallSchedulesSvgPath } = el.dataset;
return new Vue({
el,
provide: {
emptyOncallSchedulesSvgPath,
},
render(createElement) {
return createElement(OnCallSchedulesWrapper);
},
});
};
import initOnCallScheduleManagement from 'ee/oncall_schedules';
initOnCallScheduleManagement();
# frozen_string_literal: true
module Projects
module IncidentManagement
class OncallSchedulesController < Projects::ApplicationController
before_action :authorize_read_incident_management_oncall_schedule!
feature_category :incident_management
def index
end
end
end
end
...@@ -17,6 +17,13 @@ module EE ...@@ -17,6 +17,13 @@ module EE
super + %w(path_locks) super + %w(path_locks)
end end
override :sidebar_operations_paths
def sidebar_operations_paths
super + %w[
oncall_schedules
]
end
override :get_project_nav_tabs override :get_project_nav_tabs
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
nav_tabs = super nav_tabs = super
...@@ -43,6 +50,10 @@ module EE ...@@ -43,6 +50,10 @@ module EE
nav_tabs << :requirements nav_tabs << :requirements
end end
if can?(current_user, :read_incident_management_oncall_schedule, project)
nav_tabs << :oncall_schedule
end
nav_tabs nav_tabs
end end
...@@ -338,5 +349,12 @@ module EE ...@@ -338,5 +349,12 @@ module EE
} }
} }
end end
override :view_operations_tab_ability
def view_operations_tab_ability
super + [
:read_incident_management_oncall_schedule
]
end
end end
end end
# frozen_string_literal: true
module IncidentManagement
module OncallScheduleHelper
def oncall_schedule_data
{
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg')
}
end
end
end
...@@ -153,6 +153,12 @@ module EE ...@@ -153,6 +153,12 @@ module EE
!@subject.feature_available?(:feature_flags_related_issues) !@subject.feature_available?(:feature_flags_related_issues)
end end
with_scope :subject
condition(:oncall_schedules_available) do
::Feature.enabled?(:oncall_schedules_mvc, @subject) &&
@subject.feature_available?(:oncall_schedules)
end
rule { visual_review_bot }.policy do rule { visual_review_bot }.policy do
prevent :read_note prevent :read_note
enable :create_note enable :create_note
...@@ -178,9 +184,10 @@ module EE ...@@ -178,9 +184,10 @@ module EE
enable :read_deploy_board enable :read_deploy_board
enable :admin_epic_issue enable :admin_epic_issue
enable :read_group_timelogs enable :read_group_timelogs
enable :read_incident_management_oncall_schedule
end end
rule { oncall_schedules_available & can?(:reporter_access) }.enable :read_incident_management_oncall_schedule
rule { can?(:developer_access) }.policy do rule { can?(:developer_access) }.policy do
enable :admin_board enable :admin_board
enable :read_vulnerability_feedback enable :read_vulnerability_feedback
...@@ -242,11 +249,12 @@ module EE ...@@ -242,11 +249,12 @@ module EE
enable :modify_auto_fix_setting enable :modify_auto_fix_setting
enable :modify_merge_request_author_setting enable :modify_merge_request_author_setting
enable :modify_merge_request_committer_setting enable :modify_merge_request_committer_setting
enable :admin_incident_management_oncall_schedule
end end
rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy rule { license_scanning_enabled & can?(:maintainer_access) }.enable :admin_software_license_policy
rule { oncall_schedules_available & can?(:maintainer_access) }.enable :admin_incident_management_oncall_schedule
rule { auditor }.policy do rule { auditor }.policy do
enable :public_user_access enable :public_user_access
prevent :request_access prevent :request_access
......
- page_title _('On-call schedules')
#js-oncall_schedule{ data: oncall_schedule_data }
- return unless project_nav_tab? :oncall_schedule
= nav_link(controller: :oncall_schedules) do
= link_to project_incident_management_oncall_schedules_path(@project), title: _('On-call Schedules') do
%span
= _('On-call Schedules')
...@@ -120,6 +120,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -120,6 +120,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :iterations do namespace :iterations do
resources :inherited, only: [:show], constraints: { id: /\d+/ } resources :inherited, only: [:show], constraints: { id: /\d+/ }
end end
namespace :incident_management, path: '' do
resources :oncall_schedules, only: [:index], path: 'oncall_schedules'
end
end end
# End of the /-/ scope. # End of the /-/ scope.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::IncidentManagement::OncallSchedulesController do
let_it_be(:registered_user) { create(:user) }
let_it_be(:user_with_read_permissions) { create(:user) }
let_it_be(:user_with_admin_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
let(:current_user) { user_with_admin_permissions }
describe 'GET #index' do
let(:request) do
get :index, params: { project_id: project.to_param, namespace_id: project.namespace.to_param }
end
before do
project.add_reporter(user_with_read_permissions)
project.add_maintainer(user_with_admin_permissions)
stub_licensed_features(oncall_schedules: true)
sign_in(current_user)
end
context 'with read permissions' do
let(:current_user) { user_with_read_permissions }
it 'renders index with 200 status code' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
context 'with admin permissions' do
let(:current_user) { user_with_admin_permissions }
it 'renders index with 200 status code' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
context 'unauthorized' do
let(:current_user) { registered_user }
it 'responds with 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with feature flag off' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it 'responds with 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with unavailable feature' do
before do
stub_licensed_features(oncall_schedules: false)
end
it 'responds with 404' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import OnCallScheduleWrapper, {
i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
describe('AlertManagementEmptyState', () => {
let wrapper;
const emptyOncallSchedulesSvgPath = 'illustration/path.svg';
function mountComponent() {
wrapper = shallowMount(OnCallScheduleWrapper, {
provide: {
emptyOncallSchedulesSvgPath,
},
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findEmptyState = () => wrapper.find(GlEmptyState);
describe('Empty state', () => {
it('shows empty state and passed correct attributes to it', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().attributes('title')).toBe(i18n.emptyState.title);
expect(findEmptyState().attributes('description')).toBe(i18n.emptyState.description);
expect(findEmptyState().attributes('svgpath')).toBe(emptyOncallSchedulesSvgPath);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::OncallScheduleHelper do
let_it_be(:project) { create(:project) }
describe '#oncall_schedule_data' do
subject(:data) { helper.oncall_schedule_data }
it 'returns on-call schedule data' do
is_expected.to eq(
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg')
)
end
end
end
...@@ -254,6 +254,7 @@ RSpec.describe ProjectsHelper do ...@@ -254,6 +254,7 @@ RSpec.describe ProjectsHelper do
:read_licenses | [:licenses] :read_licenses | [:licenses]
:read_project_security_dashboard | [:security, :security_configuration] :read_project_security_dashboard | [:security, :security_configuration]
:read_threat_monitoring | [:threat_monitoring] :read_threat_monitoring | [:threat_monitoring]
:read_incident_management_oncall_schedule | [:oncall_schedule]
end end
with_them do with_them do
...@@ -352,4 +353,38 @@ RSpec.describe ProjectsHelper do ...@@ -352,4 +353,38 @@ RSpec.describe ProjectsHelper do
it { expect(helper.scheduled_for_deletion?(archived_project)).to be true } it { expect(helper.scheduled_for_deletion?(archived_project)).to be true }
end end
end end
describe '#can_view_operations_tab?' do
let_it_be(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(false)
end
subject { helper.send(:can_view_operations_tab?, user, project) }
where(:ability) do
[
:read_incident_management_oncall_schedule
]
end
with_them do
it 'includes operations tab' do
allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
is_expected.to be(true)
end
context 'when operations feature is disabled' do
it 'does not include operations tab' do
allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
project.project_feature.update_attribute(:operations_access_level, ProjectFeature::DISABLED)
is_expected.to be(false)
end
end
end
end
end end
...@@ -9,6 +9,11 @@ RSpec.describe IncidentManagement::OncallSchedulePolicy do ...@@ -9,6 +9,11 @@ RSpec.describe IncidentManagement::OncallSchedulePolicy do
subject(:policy) { described_class.new(user, oncall_schedule) } subject(:policy) { described_class.new(user, oncall_schedule) }
before do
stub_licensed_features(oncall_schedules: true)
stub_feature_flags(oncall_schedules_mvc: project)
end
describe 'rules' do describe 'rules' do
it { is_expected.to be_disallowed :read_incident_management_oncall_schedule } it { is_expected.to be_disallowed :read_incident_management_oncall_schedule }
......
...@@ -1360,12 +1360,29 @@ RSpec.describe ProjectPolicy do ...@@ -1360,12 +1360,29 @@ RSpec.describe ProjectPolicy do
before do before do
enable_admin_mode!(current_user) if admin_mode enable_admin_mode!(current_user) if admin_mode
stub_licensed_features(oncall_schedules: true)
end end
with_them do with_them do
let(:current_user) { public_send(role) } let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
context 'with disabled feature flag' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to(be_disallowed(policy)) }
end
context 'with unavailable license' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to(be_disallowed(policy)) }
end
end end
end end
...@@ -1384,12 +1401,29 @@ RSpec.describe ProjectPolicy do ...@@ -1384,12 +1401,29 @@ RSpec.describe ProjectPolicy do
before do before do
enable_admin_mode!(current_user) if admin_mode enable_admin_mode!(current_user) if admin_mode
stub_licensed_features(oncall_schedules: true)
end end
with_them do with_them do
let(:current_user) { public_send(role) } let(:current_user) { public_send(role) }
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
context 'with disabled feature flag' do
before do
stub_feature_flags(oncall_schedules_mvc: false)
end
it { is_expected.to(be_disallowed(policy)) }
end
context 'with unavailable license' do
before do
stub_licensed_features(oncall_schedules: false)
end
it { is_expected.to(be_disallowed(policy)) }
end
end end
end end
end end
......
...@@ -18991,6 +18991,21 @@ msgstr "" ...@@ -18991,6 +18991,21 @@ msgstr ""
msgid "On track" msgid "On track"
msgstr "" msgstr ""
msgid "On-call Schedules"
msgstr ""
msgid "On-call schedules"
msgstr ""
msgid "OnCallSchedules|Add a schedule"
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr ""
msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
......
...@@ -493,6 +493,7 @@ RSpec.describe ProjectsHelper do ...@@ -493,6 +493,7 @@ RSpec.describe ProjectsHelper do
subject { helper.send(:can_view_operations_tab?, user, project) } subject { helper.send(:can_view_operations_tab?, user, project) }
where(:ability) do
[ [
:metrics_dashboard, :metrics_dashboard,
:read_alert_management_alert, :read_alert_management_alert,
...@@ -500,7 +501,10 @@ RSpec.describe ProjectsHelper do ...@@ -500,7 +501,10 @@ RSpec.describe ProjectsHelper do
:read_issue, :read_issue,
:read_sentry_issue, :read_sentry_issue,
:read_cluster :read_cluster
].each do |ability| ]
end
with_them do
it 'includes operations tab' do it 'includes operations tab' do
allow(helper).to receive(:can?).with(user, ability, project).and_return(true) allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
......
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