Commit a68e88e7 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '287845-create-add-compliance-framework-page' into 'master'

Create add compliance framework page

See merge request gitlab-org/gitlab!54224
parents ff21bade 7a4d00dc
<script> <script>
import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; import { GlAlert, GlButton, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
...@@ -16,12 +16,17 @@ export default { ...@@ -16,12 +16,17 @@ export default {
DeleteModal, DeleteModal,
EmptyState, EmptyState,
GlAlert, GlAlert,
GlButton,
GlLoadingIcon, GlLoadingIcon,
ListItem, ListItem,
GlTab, GlTab,
GlTabs, GlTabs,
}, },
props: { props: {
addFrameworkPath: {
type: String,
required: true,
},
emptyStateSvgPath: { emptyStateSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -121,6 +126,7 @@ export default { ...@@ -121,6 +126,7 @@ export default {
), ),
allTab: s__('ComplianceFrameworks|All'), allTab: s__('ComplianceFrameworks|All'),
regulatedTab: s__('ComplianceFrameworks|Regulated'), regulatedTab: s__('ComplianceFrameworks|Regulated'),
addBtn: s__('ComplianceFrameworks|Add framework'),
}, },
}; };
</script> </script>
...@@ -135,7 +141,11 @@ export default { ...@@ -135,7 +141,11 @@ export default {
{{ alertMessage }} {{ alertMessage }}
</gl-alert> </gl-alert>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<empty-state v-if="isEmpty" :image-path="emptyStateSvgPath" /> <empty-state
v-if="isEmpty"
:image-path="emptyStateSvgPath"
:add-framework-path="addFrameworkPath"
/>
<gl-tabs v-if="hasFrameworks"> <gl-tabs v-if="hasFrameworks">
<gl-tab class="gl-mt-6" :title="$options.i18n.allTab"> <gl-tab class="gl-mt-6" :title="$options.i18n.allTab">
...@@ -148,6 +158,16 @@ export default { ...@@ -148,6 +158,16 @@ export default {
/> />
</gl-tab> </gl-tab>
<gl-tab disabled :title="$options.i18n.regulatedTab" /> <gl-tab disabled :title="$options.i18n.regulatedTab" />
<template #tabs-end>
<gl-button
class="gl-align-self-center gl-ml-auto"
category="primary"
variant="confirm"
:href="addFrameworkPath"
>
{{ $options.i18n.addBtn }}
</gl-button>
</template>
</gl-tabs> </gl-tabs>
<delete-modal <delete-modal
v-if="hasFrameworks" v-if="hasFrameworks"
......
...@@ -11,6 +11,10 @@ export default { ...@@ -11,6 +11,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
addFrameworkPath: {
type: String,
required: true,
},
}, },
i18n: { i18n: {
heading: s__('ComplianceFrameworks|There are no compliance frameworks set up yet'), heading: s__('ComplianceFrameworks|There are no compliance frameworks set up yet'),
...@@ -27,7 +31,7 @@ export default { ...@@ -27,7 +31,7 @@ export default {
:title="$options.i18n.heading" :title="$options.i18n.heading"
:description="$options.i18n.description" :description="$options.i18n.description"
:svg-path="imagePath" :svg-path="imagePath"
primary-button-link="#" :primary-button-link="addFrameworkPath"
:primary-button-text="$options.i18n.addButton" :primary-button-text="$options.i18n.addButton"
compact compact
:svg-height="110" :svg-height="110"
......
...@@ -15,7 +15,7 @@ const createComplianceFrameworksListApp = (el) => { ...@@ -15,7 +15,7 @@ const createComplianceFrameworksListApp = (el) => {
return false; return false;
} }
const { emptyStateSvgPath, groupPath } = el.dataset; const { addFrameworkPath, emptyStateSvgPath, groupPath } = el.dataset;
return new Vue({ return new Vue({
el, el,
...@@ -23,6 +23,7 @@ const createComplianceFrameworksListApp = (el) => { ...@@ -23,6 +23,7 @@ const createComplianceFrameworksListApp = (el) => {
render(createElement) { render(createElement) {
return createElement(Form, { return createElement(Form, {
props: { props: {
addFrameworkPath,
emptyStateSvgPath, emptyStateSvgPath,
groupPath, groupPath,
}, },
......
import { createComplianceFrameworksFormApp } from 'ee/groups/settings/compliance_frameworks/init_form';
createComplianceFrameworksFormApp(document.getElementById('js-compliance-frameworks-form'));
# frozen_string_literal: true
class Groups::ComplianceFrameworksController < Groups::ApplicationController
extend ActiveSupport::Concern
before_action :check_group_compliance_frameworks_available!
before_action :authorize_admin_group!
feature_category :compliance_management
def new
end
protected
def check_group_compliance_frameworks_available!
render_404 unless can?(current_user, :admin_compliance_framework, group)
end
end
...@@ -4,15 +4,31 @@ module ComplianceManagement ...@@ -4,15 +4,31 @@ module ComplianceManagement
module ComplianceFramework module ComplianceFramework
module GroupSettingsHelper module GroupSettingsHelper
def show_compliance_frameworks? def show_compliance_frameworks?
current_user.can?(:admin_compliance_framework, @group) can?(current_user, :admin_compliance_framework, @group)
end end
def compliance_frameworks_list_data def compliance_frameworks_list_data
{ {
empty_state_svg_path: image_path('illustrations/welcome/ee_trial.svg'), empty_state_svg_path: image_path('illustrations/welcome/ee_trial.svg'),
group_path: @group.full_path group_path: @group.full_path,
add_framework_path: new_group_compliance_framework_path(@group)
} }
end end
def compliance_frameworks_new_form_data
{
group_path: @group.full_path,
group_edit_path: edit_group_path(@group, anchor: 'js-compliance-frameworks-settings'),
graphql_field_name: ComplianceManagement::Framework.name,
pipeline_configuration_full_path_enabled: pipeline_configuration_full_path_enabled?.to_s
}
end
private
def pipeline_configuration_full_path_enabled?
can?(current_user, :admin_compliance_pipeline_configuration, @group)
end
end end
end end
end end
...@@ -128,6 +128,11 @@ module EE ...@@ -128,6 +128,11 @@ module EE
::Feature.enabled?(:ff_custom_compliance_frameworks, @subject) ::Feature.enabled?(:ff_custom_compliance_frameworks, @subject)
end end
condition(:group_level_compliance_pipeline_available) do
@subject.feature_available?(:evaluate_group_level_compliance_pipeline) &&
::Feature.enabled?(:ff_custom_compliance_frameworks, @subject, default_enabled: :yaml)
end
rule { public_group | logged_in_viewable }.policy do rule { public_group | logged_in_viewable }.policy do
enable :read_wiki enable :read_wiki
enable :download_wiki_code enable :download_wiki_code
...@@ -349,6 +354,7 @@ module EE ...@@ -349,6 +354,7 @@ module EE
end end
rule { can?(:owner_access) & compliance_framework_available }.enable :admin_compliance_framework rule { can?(:owner_access) & compliance_framework_available }.enable :admin_compliance_framework
rule { can?(:owner_access) & group_level_compliance_pipeline_available }.enable :admin_compliance_pipeline_configuration
end end
override :lookup_access_level! override :lookup_access_level!
......
- add_to_breadcrumbs _('General Settings'), edit_group_path(@group)
- title = s_('ComplianceFramework|New Compliance Framework')
- page_title title
%h3.page-title= title
#js-compliance-frameworks-form{ data: compliance_frameworks_new_form_data }
...@@ -11,6 +11,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -11,6 +11,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
patch :override, on: :member patch :override, on: :member
end end
resources :compliance_frameworks, only: [:new]
get '/analytics', to: redirect('groups/%{group_id}/-/analytics/value_stream_analytics') get '/analytics', to: redirect('groups/%{group_id}/-/analytics/value_stream_analytics')
resource :contribution_analytics, only: [:show] resource :contribution_analytics, only: [:show]
......
...@@ -11,6 +11,7 @@ describe('ListEmptyState', () => { ...@@ -11,6 +11,7 @@ describe('ListEmptyState', () => {
wrapper = shallowMount(ListEmptyState, { wrapper = shallowMount(ListEmptyState, {
propsData: { propsData: {
imagePath: 'dir/image.svg', imagePath: 'dir/image.svg',
addFrameworkPath: 'group/framework/new',
...props, ...props,
}, },
}); });
...@@ -27,7 +28,7 @@ describe('ListEmptyState', () => { ...@@ -27,7 +28,7 @@ describe('ListEmptyState', () => {
title: 'There are no compliance frameworks set up yet', title: 'There are no compliance frameworks set up yet',
description: 'Once you have created a compliance framework it will appear here.', description: 'Once you have created a compliance framework it will appear here.',
svgPath: 'dir/image.svg', svgPath: 'dir/image.svg',
primaryButtonLink: '#', primaryButtonLink: 'group/framework/new',
primaryButtonText: 'Add framework', primaryButtonText: 'Add framework',
svgHeight: 110, svgHeight: 110,
compact: true, compact: true,
......
import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; import { GlAlert, GlButton, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
...@@ -31,6 +31,7 @@ describe('List', () => { ...@@ -31,6 +31,7 @@ describe('List', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(EmptyState); const findEmptyState = () => wrapper.find(EmptyState);
const findTabs = () => wrapper.findAll(GlTab); const findTabs = () => wrapper.findAll(GlTab);
const findAddBtn = () => wrapper.find(GlButton);
const findTabsContainer = () => wrapper.find(GlTabs); const findTabsContainer = () => wrapper.find(GlTabs);
const findListItems = () => wrapper.findAll(ListItem); const findListItems = () => wrapper.findAll(ListItem);
...@@ -47,6 +48,7 @@ describe('List', () => { ...@@ -47,6 +48,7 @@ describe('List', () => {
localVue, localVue,
apolloProvider: createMockApolloProvider(resolverMock), apolloProvider: createMockApolloProvider(resolverMock),
propsData: { propsData: {
addFrameworkPath: 'group/framework/new',
emptyStateSvgPath: 'dir/image.svg', emptyStateSvgPath: 'dir/image.svg',
groupPath: 'group-1', groupPath: 'group-1',
}, },
...@@ -118,6 +120,7 @@ describe('List', () => { ...@@ -118,6 +120,7 @@ describe('List', () => {
it('shows the empty state', () => { it('shows the empty state', () => {
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().props('imagePath')).toBe('dir/image.svg'); expect(findEmptyState().props('imagePath')).toBe('dir/image.svg');
expect(findEmptyState().props('addFrameworkPath')).toBe('group/framework/new');
}); });
it('does not show the other parts of the app', () => { it('does not show the other parts of the app', () => {
...@@ -155,6 +158,13 @@ describe('List', () => { ...@@ -155,6 +158,13 @@ describe('List', () => {
expect(tab.attributes('disabled')).toBe('true'); expect(tab.attributes('disabled')).toBe('true');
}); });
it('shows the add framework button', () => {
const addBtn = findAddBtn();
expect(addBtn.attributes('href')).toBe('group/framework/new');
expect(addBtn.text()).toBe('Add framework');
});
it('shows the list items with expect props', () => { it('shows the list items with expect props', () => {
expect(findListItems()).toHaveLength(2); expect(findListItems()).toHaveLength(2);
......
...@@ -12,24 +12,22 @@ RSpec.describe ComplianceManagement::ComplianceFramework::GroupSettingsHelper do ...@@ -12,24 +12,22 @@ RSpec.describe ComplianceManagement::ComplianceFramework::GroupSettingsHelper do
end end
describe '#show_compliance_frameworks?' do describe '#show_compliance_frameworks?' do
using RSpec::Parameterized::TableSyntax subject { helper.show_compliance_frameworks? }
where(:feature_flag_enabled, :license_feature_enabled, :result) do context 'the user has permission' do
true | true | true before do
false | true | false allow(helper).to receive(:can?).with(current_user, :admin_compliance_framework, group).and_return(true)
true | false | false end
false | false | false
it { is_expected.to be true }
end end
with_them do context 'the user does not have permission' do
before do before do
stub_feature_flags(ff_custom_compliance_frameworks: feature_flag_enabled) allow(helper).to receive(:can?).with(current_user, :admin_compliance_framework, group).and_return(false)
stub_licensed_features(custom_compliance_frameworks: license_feature_enabled)
end end
it 'returns the correct value' do it { is_expected.to be false }
expect(helper.show_compliance_frameworks?).to eql(result)
end
end end
end end
...@@ -37,8 +35,36 @@ RSpec.describe ComplianceManagement::ComplianceFramework::GroupSettingsHelper do ...@@ -37,8 +35,36 @@ RSpec.describe ComplianceManagement::ComplianceFramework::GroupSettingsHelper do
it 'returns the correct data' do it 'returns the correct data' do
expect(helper.compliance_frameworks_list_data).to contain_exactly( expect(helper.compliance_frameworks_list_data).to contain_exactly(
[:empty_state_svg_path, ActionController::Base.helpers.image_path('illustrations/welcome/ee_trial.svg')], [:empty_state_svg_path, ActionController::Base.helpers.image_path('illustrations/welcome/ee_trial.svg')],
[:group_path, group.full_path] [:group_path, group.full_path],
[:add_framework_path, new_group_compliance_framework_path(group)]
) )
end end
end end
describe '#compliance_frameworks_new_form_data' do
subject { helper.compliance_frameworks_new_form_data }
shared_examples 'returns the correct data' do |pipeline_configuration_enabled|
before do
allow(helper).to receive(:can?).with(current_user, :admin_compliance_pipeline_configuration, group).and_return(pipeline_configuration_enabled)
end
it {
is_expected.to contain_exactly(
[:group_path, group.full_path],
[:group_edit_path, edit_group_path(group, anchor: 'js-compliance-frameworks-settings')],
[:graphql_field_name, ComplianceManagement::Framework.name],
[:pipeline_configuration_full_path_enabled, pipeline_configuration_enabled.to_s]
)
}
end
context 'the user has pipeline configuration permission' do
it_behaves_like 'returns the correct data', [true]
end
context 'the user does not have pipeline configuration permission' do
it_behaves_like 'returns the correct data', [false]
end
end
end end
...@@ -1447,12 +1447,12 @@ RSpec.describe GroupPolicy do ...@@ -1447,12 +1447,12 @@ RSpec.describe GroupPolicy do
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end end
end end
end
describe ':admin_compliance_framework' do describe 'compliance framework permissions' do
shared_context 'compliance framework permissions' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:policy) { :admin_compliance_framework }
where(:role, :licensed, :feature_flag, :allowed) do where(:role, :licensed, :feature_flag, :allowed) do
:owner | true | true | true :owner | true | true | true
:owner | true | false | false :owner | true | false | false
...@@ -1469,12 +1469,26 @@ RSpec.describe GroupPolicy do ...@@ -1469,12 +1469,26 @@ RSpec.describe GroupPolicy do
let(:current_user) { public_send(role) } let(:current_user) { public_send(role) }
before do before do
stub_licensed_features(custom_compliance_frameworks: licensed) stub_licensed_features(licensed_feature => licensed)
stub_feature_flags(ff_custom_compliance_frameworks: feature_flag) stub_feature_flags(ff_custom_compliance_frameworks: feature_flag)
end end
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end end
end end
context ':admin_compliance_framework' do
let(:policy) { :admin_compliance_framework }
let(:licensed_feature) { :custom_compliance_frameworks }
include_context 'compliance framework permissions'
end
context ':admin_compliance_pipeline_configuration' do
let(:policy) { :admin_compliance_pipeline_configuration }
let(:licensed_feature) { :evaluate_group_level_compliance_pipeline }
include_context 'compliance framework permissions'
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'group compliance frameworks' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
before do
login_as(user)
end
context 'when compliance frameworks feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
stub_licensed_features(custom_compliance_frameworks: false)
end
describe 'GET /groups/:group/-/compliance_frameworks/new' do
it 'returns 404 not found' do
get new_group_compliance_framework_path(group)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when compliance frameworks feature is enabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: true)
stub_licensed_features(custom_compliance_frameworks: true)
end
describe 'GET /groups/:group/-/compliance_frameworks/new' do
it 'renders template' do
group.add_owner(user)
get new_group_compliance_framework_path(group)
expect(response).to render_template 'groups/compliance_frameworks/new'
end
context 'with unauthorized user' do
it 'returns 404 not found' do
get new_group_compliance_framework_path(group)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/compliance_frameworks/new.html.haml' do
let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
before do
assign(:group, group)
allow(view).to receive(:current_user).and_return(user)
allow(user).to receive(:can?).with(:admin_compliance_pipeline_configuration, group).and_return(true)
end
it 'shows the compliance frameworks form', :aggregate_failures do
render
expect(rendered).to have_content('New Compliance Framework')
expect(rendered).to have_css('#js-compliance-frameworks-form')
end
end
...@@ -7689,6 +7689,9 @@ msgstr "" ...@@ -7689,6 +7689,9 @@ msgstr ""
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act" msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr "" msgstr ""
msgid "ComplianceFramework|New Compliance Framework"
msgstr ""
msgid "ComplianceFramework|PCI-DSS" msgid "ComplianceFramework|PCI-DSS"
msgstr "" msgstr ""
......
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