Commit 150af52b authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'tr-on-call-scheduling' into 'master'

Build on-call schedule empty state

See merge request gitlab-org/gitlab!46639
parents 4436c579 944a517e
......@@ -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