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
}
end
def can_view_operations_tab?(current_user, project)
return false unless project.feature_available?(:operations, current_user)
def view_operations_tab_ability
[
:metrics_dashboard,
:read_alert_management_alert,
......@@ -479,7 +477,13 @@ module ProjectsHelper
:read_cluster,
:read_feature_flag,
: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)
end
end
......
......@@ -262,6 +262,8 @@
%span
= _('Incidents')
= render_if_exists 'projects/sidebar/oncall_schedules'
- if project_nav_tab? :serverless
= nav_link(controller: :functions) 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
super + %w(path_locks)
end
override :sidebar_operations_paths
def sidebar_operations_paths
super + %w[
oncall_schedules
]
end
override :get_project_nav_tabs
def get_project_nav_tabs(project, current_user)
nav_tabs = super
......@@ -43,6 +50,10 @@ module EE
nav_tabs << :requirements
end
if can?(current_user, :read_incident_management_oncall_schedule, project)
nav_tabs << :oncall_schedule
end
nav_tabs
end
......@@ -338,5 +349,12 @@ module EE
}
}
end
override :view_operations_tab_ability
def view_operations_tab_ability
super + [
:read_incident_management_oncall_schedule
]
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
!@subject.feature_available?(:feature_flags_related_issues)
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
prevent :read_note
enable :create_note
......@@ -178,9 +184,10 @@ module EE
enable :read_deploy_board
enable :admin_epic_issue
enable :read_group_timelogs
enable :read_incident_management_oncall_schedule
end
rule { oncall_schedules_available & can?(:reporter_access) }.enable :read_incident_management_oncall_schedule
rule { can?(:developer_access) }.policy do
enable :admin_board
enable :read_vulnerability_feedback
......@@ -242,11 +249,12 @@ module EE
enable :modify_auto_fix_setting
enable :modify_merge_request_author_setting
enable :modify_merge_request_committer_setting
enable :admin_incident_management_oncall_schedule
end
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
enable :public_user_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
namespace :iterations do
resources :inherited, only: [:show], constraints: { id: /\d+/ }
end
namespace :incident_management, path: '' do
resources :oncall_schedules, only: [:index], path: 'oncall_schedules'
end
end
# 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
......@@ -249,11 +249,12 @@ RSpec.describe ProjectsHelper do
using RSpec::Parameterized::TableSyntax
where(:ability, :nav_tabs) do
:read_dependencies | [:dependencies]
:read_feature_flag | [:operations]
:read_licenses | [:licenses]
:read_project_security_dashboard | [:security, :security_configuration]
:read_threat_monitoring | [:threat_monitoring]
:read_dependencies | [:dependencies]
:read_feature_flag | [:operations]
:read_licenses | [:licenses]
:read_project_security_dashboard | [:security, :security_configuration]
:read_threat_monitoring | [:threat_monitoring]
:read_incident_management_oncall_schedule | [:oncall_schedule]
end
with_them do
......@@ -352,4 +353,38 @@ RSpec.describe ProjectsHelper do
it { expect(helper.scheduled_for_deletion?(archived_project)).to be true }
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
......@@ -9,6 +9,11 @@ RSpec.describe IncidentManagement::OncallSchedulePolicy do
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
it { is_expected.to be_disallowed :read_incident_management_oncall_schedule }
......
......@@ -1360,12 +1360,29 @@ RSpec.describe ProjectPolicy do
before do
enable_admin_mode!(current_user) if admin_mode
stub_licensed_features(oncall_schedules: true)
end
with_them do
let(:current_user) { public_send(role) }
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
......@@ -1384,12 +1401,29 @@ RSpec.describe ProjectPolicy do
before do
enable_admin_mode!(current_user) if admin_mode
stub_licensed_features(oncall_schedules: true)
end
with_them do
let(:current_user) { public_send(role) }
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
......
......@@ -18991,6 +18991,21 @@ msgstr ""
msgid "On track"
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."
msgstr ""
......
......@@ -493,14 +493,18 @@ RSpec.describe ProjectsHelper do
subject { helper.send(:can_view_operations_tab?, user, project) }
[
:metrics_dashboard,
:read_alert_management_alert,
:read_environment,
:read_issue,
:read_sentry_issue,
:read_cluster
].each do |ability|
where(:ability) do
[
:metrics_dashboard,
:read_alert_management_alert,
:read_environment,
:read_issue,
:read_sentry_issue,
:read_cluster
]
end
with_them do
it 'includes operations tab' do
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