From 78c8851d4403f878076c698a6b6cc528db019ff3 Mon Sep 17 00:00:00 2001 From: Robert Hunt <rhunt@gitlab.com> Date: Mon, 26 Jul 2021 14:42:27 +0100 Subject: [PATCH] Add the compliance framework label to group projects listing - Add compliance framework to entity - Add compliance framework label component to group item - Add compliance framework to store - Create tests Changelog: added MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66819 EE: true --- .../groups/components/group_item.vue | 17 +++ .../javascripts/groups/store/groups_store.js | 15 +- ee/app/serializers/ee/group_child_entity.rb | 10 ++ .../groups/components/group_item_spec.js | 48 ++++++ ee/spec/frontend/groups/mock_data.js | 140 ++++++++++++++++++ .../groups/store/groups_store_spec.js | 24 +++ .../serializers/ee/group_child_entity_spec.rb | 75 ++++++++++ .../groups/components/group_item_spec.js | 5 +- 8 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 ee/spec/frontend/groups/components/group_item_spec.js create mode 100644 ee/spec/frontend/groups/mock_data.js create mode 100644 ee/spec/frontend/groups/store/groups_store_spec.js create mode 100644 ee/spec/serializers/ee/group_child_entity_spec.rb diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index ad0b27c969379..10c45abbfa2c3 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -28,6 +28,10 @@ export default { GlLoadingIcon, GlIcon, UserAccessRoleBadge, + ComplianceFrameworkLabel: () => + import( + 'ee_component/vue_shared/components/compliance_framework_label/compliance_framework_label.vue' + ), itemCaret, itemTypeIcon, itemStats, @@ -67,6 +71,9 @@ export default { hasAvatar() { return this.group.avatarUrl !== null; }, + hasComplianceFramework() { + return Boolean(this.group.complianceFramework?.name); + }, isGroup() { return this.group.type === 'group'; }, @@ -82,6 +89,9 @@ export default { microdata() { return this.group.microdata || {}; }, + complianceFramework() { + return this.group.complianceFramework; + }, }, methods: { onClickRowGroup(e) { @@ -167,6 +177,13 @@ export default { <user-access-role-badge v-if="group.permission" class="gl-mt-3"> {{ group.permission }} </user-access-role-badge> + <compliance-framework-label + v-if="hasComplianceFramework" + class="gl-mt-3" + :name="complianceFramework.name" + :color="complianceFramework.color" + :description="complianceFramework.description" + /> </div> <div v-if="group.description" class="description"> <span diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index 6cf70f4052ef8..93fbd8be47d5d 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -1,4 +1,5 @@ -import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils'; +import { isEmpty } from 'lodash'; +import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; import { getGroupItemMicrodata } from './utils'; export default class GroupsStore { @@ -70,7 +71,7 @@ export default class GroupsStore { ? rawGroupItem.subgroup_count : rawGroupItem.children_count; - return { + const groupItem = { id: rawGroupItem.id, name: rawGroupItem.name, fullName: rawGroupItem.full_name, @@ -98,6 +99,16 @@ export default class GroupsStore { pendingRemoval: rawGroupItem.marked_for_deletion, microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {}, }; + + if (!isEmpty(rawGroupItem.compliance_management_framework)) { + groupItem.complianceFramework = { + name: rawGroupItem.compliance_management_framework.name, + color: rawGroupItem.compliance_management_framework.color, + description: rawGroupItem.compliance_management_framework.description, + }; + } + + return groupItem; } removeGroup(group, parentGroup) { diff --git a/ee/app/serializers/ee/group_child_entity.rb b/ee/app/serializers/ee/group_child_entity.rb index 4a53f0ff62014..4c414a51c4415 100644 --- a/ee/app/serializers/ee/group_child_entity.rb +++ b/ee/app/serializers/ee/group_child_entity.rb @@ -9,6 +9,16 @@ module GroupChildEntity expose :marked_for_deletion do |instance| instance.marked_for_deletion? end + + expose :compliance_management_framework, if: lambda { |_instance, _options| compliance_framework_available? } + end + + private + + def compliance_framework_available? + return unless project? + + object.licensed_feature_available?(:compliance_framework) end end end diff --git a/ee/spec/frontend/groups/components/group_item_spec.js b/ee/spec/frontend/groups/components/group_item_spec.js new file mode 100644 index 0000000000000..7d59805a3fd8f --- /dev/null +++ b/ee/spec/frontend/groups/components/group_item_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import ComplianceFrameworkLabel from 'ee_component/vue_shared/components/compliance_framework_label/compliance_framework_label.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import GroupFolder from '~/groups/components/group_folder.vue'; +import GroupItem from '~/groups/components/group_item.vue'; +import { mockParentGroupItem, mockChildren } from '../mock_data'; + +const createComponent = (props = {}) => { + return shallowMount(GroupItem, { + propsData: { + parentGroup: mockParentGroupItem, + ...props, + }, + components: { GroupFolder }, + }); +}; + +describe('GroupItemComponent', () => { + let wrapper; + + const findComplianceFrameworkLabel = () => wrapper.findComponent(ComplianceFrameworkLabel); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Compliance framework label', () => { + it('does not render if the item does not have a compliance framework', async () => { + wrapper = createComponent({ group: mockChildren[0] }); + await waitForPromises(); + + expect(findComplianceFrameworkLabel().exists()).toBe(false); + }); + + it('renders if the item has a compliance framework', async () => { + const { color, description, name } = mockChildren[1].complianceFramework; + + wrapper = createComponent({ group: mockChildren[1] }); + await waitForPromises(); + + expect(findComplianceFrameworkLabel().props()).toStrictEqual({ + color, + description, + name, + }); + }); + }); +}); diff --git a/ee/spec/frontend/groups/mock_data.js b/ee/spec/frontend/groups/mock_data.js new file mode 100644 index 0000000000000..49dda29825f3c --- /dev/null +++ b/ee/spec/frontend/groups/mock_data.js @@ -0,0 +1,140 @@ +export const mockParentGroupItem = { + id: 55, + name: 'hardware', + description: '', + visibility: 'public', + fullName: 'platform / hardware', + relativePath: '/platform/hardware', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/edit', + childrenCount: 3, + leavePath: '/groups/platform/hardware/group_members/leave', + parentId: 54, + memberCount: '1', + projectCount: 1, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', +}; + +export const mockChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + fullName: 'platform / hardware / bsp', + relativePath: '/platform/hardware/bsp', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/bsp/edit', + childrenCount: 6, + leavePath: '/groups/platform/hardware/bsp/group_members/leave', + parentId: 55, + memberCount: '1', + projectCount: 4, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', + complianceFramework: {}, + }, + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + fullName: 'platform / hardware / bsp', + relativePath: '/platform/hardware/bsp', + canEdit: true, + type: 'group', + avatarUrl: null, + permission: 'Owner', + editPath: '/groups/platform/hardware/bsp/edit', + childrenCount: 6, + leavePath: '/groups/platform/hardware/bsp/group_members/leave', + parentId: 55, + memberCount: '1', + projectCount: 4, + subgroupCount: 2, + canLeave: false, + children: [], + isOpen: true, + isChildrenLoading: false, + isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', + complianceFramework: { + name: 'GDPR', + description: 'General Data Protection Regulation', + color: '#009966', + }, + }, +]; + +export const mockRawChildren = [ + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [], + updated_at: '2017-04-09T18:40:39.101Z', + }, + { + id: 57, + name: 'bsp', + description: '', + visibility: 'public', + full_name: 'platform / hardware / bsp', + relative_path: '/platform/hardware/bsp', + can_edit: true, + type: 'group', + avatar_url: null, + permission: 'Owner', + edit_path: '/groups/platform/hardware/bsp/edit', + children_count: 6, + leave_path: '/groups/platform/hardware/bsp/group_members/leave', + parent_id: 55, + number_users_with_delimiter: '1', + project_count: 4, + subgroup_count: 2, + can_leave: false, + children: [], + updated_at: '2017-04-09T18:40:39.101Z', + compliance_management_framework: { + id: 1, + namespace_id: 1, + name: 'GDPR', + description: 'General Data Protection Regulation', + color: '#009966', + pipeline_configuration_full_path: null, + regulated: true, + }, + }, +]; diff --git a/ee/spec/frontend/groups/store/groups_store_spec.js b/ee/spec/frontend/groups/store/groups_store_spec.js new file mode 100644 index 0000000000000..bcfe11bad5f85 --- /dev/null +++ b/ee/spec/frontend/groups/store/groups_store_spec.js @@ -0,0 +1,24 @@ +import GroupsStore from '~/groups/store/groups_store'; +import { mockRawChildren } from '../mock_data'; + +describe('ee/ProjectsStore', () => { + describe('formatGroupItem', () => { + it('without a compliance framework', () => { + const store = new GroupsStore(); + const updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); + + expect(updatedGroupItem.complianceFramework).toBeUndefined(); + }); + + it('with a compliance framework', () => { + const store = new GroupsStore(); + const updatedGroupItem = store.formatGroupItem(mockRawChildren[1]); + + expect(updatedGroupItem.complianceFramework).toStrictEqual({ + name: mockRawChildren[1].compliance_management_framework.name, + color: mockRawChildren[1].compliance_management_framework.color, + description: mockRawChildren[1].compliance_management_framework.description, + }); + }); + }); +}); diff --git a/ee/spec/serializers/ee/group_child_entity_spec.rb b/ee/spec/serializers/ee/group_child_entity_spec.rb new file mode 100644 index 0000000000000..c593b69406abc --- /dev/null +++ b/ee/spec/serializers/ee/group_child_entity_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GroupChildEntity do + include ExternalAuthorizationServiceHelpers + include Gitlab::Routing.url_helpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :with_sox_compliance_framework) } + let_it_be(:project_without_compliance_framework) { create(:project) } + let_it_be(:group) { create(:group) } + + let(:request) { double('request') } + let(:entity) { described_class.new(object, request: request) } + + subject(:json) { entity.as_json } + + before do + allow(request).to receive(:current_user).and_return(user) + stub_commonmark_sourcepos_disabled + end + + describe 'with compliance framework' do + shared_examples 'does not have the compliance framework' do + it do + expect(json[:compliance_management_framework]).to be_nil + end + end + + context 'disabled' do + before do + stub_licensed_features(compliance_framework: false) + end + + context 'for a project' do + let(:object) { project } + + it_behaves_like 'does not have the compliance framework' + end + + context 'for a group' do + let(:object) { group } + + it_behaves_like 'does not have the compliance framework' + end + end + + describe 'enabled' do + before do + stub_licensed_features(compliance_framework: true) + end + + context 'for a project' do + let(:object) { project } + + it 'has the compliance framework' do + expect(json[:compliance_management_framework]['name']).to eq('SOX') + end + end + + context 'for a project without a compliance framework' do + let(:object) { project_without_compliance_framework } + + it_behaves_like 'does not have the compliance framework' + end + + context 'for a group' do + let(:object) { group } + + it_behaves_like 'does not have the compliance framework' + end + end + end +end diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 2369685f50626..60d47895a959d 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import waitForPromises from 'helpers/wait_for_promises'; import GroupFolder from '~/groups/components/group_folder.vue'; import GroupItem from '~/groups/components/group_item.vue'; import ItemActions from '~/groups/components/item_actions.vue'; @@ -22,8 +22,7 @@ describe('GroupItemComponent', () => { beforeEach(() => { wrapper = createComponent(); - - return Vue.nextTick(); + return waitForPromises(); }); afterEach(() => { -- GitLab