Commit 4b95c248 authored by Jiaan Louw's avatar Jiaan Louw Committed by Phil Hughes

Add merge request violation component for compliance reports

parent 43659d07
...@@ -735,3 +735,14 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag]; ...@@ -735,3 +735,14 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i)); export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
export const isLoggedIn = () => Boolean(window.gon?.current_user_id); export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
/**
* This method takes in array of objects with snake_case
* property names and returns a new array of objects with
* camelCase property names
*
* @param {Array[Object]} array - Array to be converted
* @returns {Array[Object]} Converted array
*/
export const convertArrayOfObjectsToCamelCase = (array) =>
array.map((o) => convertObjectPropsToCamelCase(o));
<script> <script>
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { convertArrayOfObjectsToCamelCase } from '~/lib/utils/common_utils';
import { COMPLIANCE_DRAWER_CONTAINER_CLASS } from '../constants'; import { COMPLIANCE_DRAWER_CONTAINER_CLASS } from '../constants';
import { getContentWrapperHeight } from '../../threat_monitoring/utils'; import { getContentWrapperHeight } from '../../threat_monitoring/utils';
import BranchPath from './drawer_sections/branch_path.vue'; import BranchPath from './drawer_sections/branch_path.vue';
...@@ -38,6 +39,15 @@ export default { ...@@ -38,6 +39,15 @@ export default {
drawerHeaderHeight() { drawerHeaderHeight() {
return getContentWrapperHeight(COMPLIANCE_DRAWER_CONTAINER_CLASS); return getContentWrapperHeight(COMPLIANCE_DRAWER_CONTAINER_CLASS);
}, },
committers() {
return convertArrayOfObjectsToCamelCase(this.mergeRequest.committers);
},
approvedByUsers() {
return convertArrayOfObjectsToCamelCase(this.mergeRequest.approved_by_users);
},
commenters() {
return convertArrayOfObjectsToCamelCase(this.mergeRequest.participants);
},
}, },
DRAWER_Z_INDEX, DRAWER_Z_INDEX,
}; };
...@@ -67,11 +77,8 @@ export default { ...@@ -67,11 +77,8 @@ export default {
:target-branch="mergeRequest.target_branch" :target-branch="mergeRequest.target_branch"
:target-branch-uri="mergeRequest.target_branch_uri" :target-branch-uri="mergeRequest.target_branch_uri"
/> />
<committers :committers="mergeRequest.committers" /> <committers :committers="committers" />
<reviewers <reviewers :approvers="approvedByUsers" :commenters="commenters" />
:approvers="mergeRequest.approved_by_users"
:commenters="mergeRequest.participants"
/>
<merged-by :merged-by="mergeRequest.merged_by" /> <merged-by :merged-by="mergeRequest.merged_by" />
</template> </template>
</gl-drawer> </gl-drawer>
......
<script> <script>
import { GlAvatar, GlAvatarsInline, GlAvatarLink } from '@gitlab/ui'; import { GlAvatarsInline } from '@gitlab/ui';
import { DRAWER_AVATAR_SIZE, DRAWER_MAXIMUM_AVATARS } from '../../constants'; import { DRAWER_AVATAR_SIZE, DRAWER_MAXIMUM_AVATARS } from '../../constants';
import DrawerSectionSubHeader from './drawer_section_sub_header.vue'; import DrawerSectionSubHeader from './drawer_section_sub_header.vue';
import UserAvatar from './user_avatar.vue';
export default { export default {
components: { components: {
DrawerSectionSubHeader, DrawerSectionSubHeader,
GlAvatar,
GlAvatarsInline, GlAvatarsInline,
GlAvatarLink, UserAvatar,
}, },
props: { props: {
avatars: { avatars: {
...@@ -57,20 +57,7 @@ export default { ...@@ -57,20 +57,7 @@ export default {
badge-tooltip-prop="name" badge-tooltip-prop="name"
> >
<template #avatar="{ avatar }"> <template #avatar="{ avatar }">
<gl-avatar-link <user-avatar :user="avatar" />
target="blank"
:href="avatar.web_url"
:title="avatar.name"
class="js-user-link"
:data-user-id="avatar.id"
:data-name="avatar.name"
>
<gl-avatar
:src="avatar.avatar_url"
:entity-name="avatar.name"
:size="$options.DRAWER_AVATAR_SIZE"
/>
</gl-avatar-link>
</template> </template>
</gl-avatars-inline> </gl-avatars-inline>
</div> </div>
......
<script>
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { DRAWER_AVATAR_SIZE } from '../../constants';
export default {
components: {
GlAvatar,
GlAvatarLink,
},
props: {
user: {
type: Object,
required: true,
},
},
DRAWER_AVATAR_SIZE,
};
</script>
<template>
<gl-avatar-link
target="blank"
:href="user.webUrl"
:title="user.name"
class="js-user-link"
:data-user-id="user.id"
:data-name="user.name"
>
<gl-avatar :src="user.avatarUrl" :entity-name="user.name" :size="$options.DRAWER_AVATAR_SIZE" />
</gl-avatar-link>
</template>
<script>
import { MERGE_REQUEST_VIOLATION_REASONS, MERGE_REQUEST_VIOLATION_MESSAGES } from '../../constants';
import UserAvatar from '../shared/user_avatar.vue';
export default {
components: {
UserAvatar,
},
props: {
reason: {
type: Number,
required: true,
},
user: {
type: Object,
required: false,
default: null,
},
},
computed: {
violation() {
return MERGE_REQUEST_VIOLATION_REASONS[this.reason];
},
violationMessage() {
return MERGE_REQUEST_VIOLATION_MESSAGES[this.violation];
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-mr-2">{{ violationMessage }}</span>
<user-avatar v-if="user" :user="user" />
</div>
</template>
import { s__ } from '~/locale';
export const PRESENTABLE_APPROVERS_LIMIT = 2; export const PRESENTABLE_APPROVERS_LIMIT = 2;
export const COMPLIANCE_TAB_COOKIE_KEY = 'compliance_dashboard_tabs'; export const COMPLIANCE_TAB_COOKIE_KEY = 'compliance_dashboard_tabs';
...@@ -11,3 +13,19 @@ export const DRAWER_AVATAR_SIZE = 24; ...@@ -11,3 +13,19 @@ export const DRAWER_AVATAR_SIZE = 24;
export const DRAWER_MAXIMUM_AVATARS = 20; export const DRAWER_MAXIMUM_AVATARS = 20;
export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper'; export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper';
const VIOLATION_TYPE_APPROVED_BY_AUTHOR = 'approved_by_author';
const VIOLATION_TYPE_APPROVED_BY_COMMITTER = 'approved_by_committer';
const VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS = 'approved_by_insufficient_users';
export const MERGE_REQUEST_VIOLATION_REASONS = {
0: VIOLATION_TYPE_APPROVED_BY_AUTHOR,
1: VIOLATION_TYPE_APPROVED_BY_COMMITTER,
2: VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS,
};
export const MERGE_REQUEST_VIOLATION_MESSAGES = {
[VIOLATION_TYPE_APPROVED_BY_AUTHOR]: s__('ComplianceReport|Approved by author'),
[VIOLATION_TYPE_APPROVED_BY_COMMITTER]: s__('ComplianceReport|Approved by committer'),
[VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS]: s__('ComplianceReport|Less than 2 approvers'),
};
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { convertArrayOfObjectsToCamelCase } from '~/lib/utils/common_utils';
import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue'; import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue';
import BranchPath from 'ee/compliance_dashboard/components/drawer_sections/branch_path.vue'; import BranchPath from 'ee/compliance_dashboard/components/drawer_sections/branch_path.vue';
import Committers from 'ee/compliance_dashboard/components/drawer_sections/committers.vue'; import Committers from 'ee/compliance_dashboard/components/drawer_sections/committers.vue';
...@@ -112,16 +113,16 @@ describe('MergeRequestDrawer component', () => { ...@@ -112,16 +113,16 @@ describe('MergeRequestDrawer component', () => {
expect(findBranchPath().exists()).toBe(false); expect(findBranchPath().exists()).toBe(false);
}); });
it('has the committers section', () => { it('has the committers section with users array converted to camel case', () => {
expect(findCommitters().props()).toStrictEqual({ expect(findCommitters().props()).toStrictEqual({
committers: mergeRequest.committers, committers: convertArrayOfObjectsToCamelCase(mergeRequest.committers),
}); });
}); });
it('has the reviewers section', () => { it('has the reviewers section with users array converted to camel case', () => {
expect(findReviewers().props()).toStrictEqual({ expect(findReviewers().props()).toStrictEqual({
approvers: mergeRequest.approved_by_users, approvers: convertArrayOfObjectsToCamelCase(mergeRequest.approved_by_users),
commenters: mergeRequest.participants, commenters: convertArrayOfObjectsToCamelCase(mergeRequest.participants),
}); });
}); });
......
import { GlAvatar, GlAvatarLink, GlAvatarsInline } from '@gitlab/ui'; import { GlAvatarsInline } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import DrawerAvatarsList from 'ee/compliance_dashboard/components/shared/drawer_avatars_list.vue'; import DrawerAvatarsList from 'ee/compliance_dashboard/components/shared/drawer_avatars_list.vue';
import UserAvatar from 'ee/compliance_dashboard/components/shared//user_avatar.vue';
import DrawerSectionSubHeader from 'ee/compliance_dashboard/components/shared/drawer_section_sub_header.vue'; import DrawerSectionSubHeader from 'ee/compliance_dashboard/components/shared/drawer_section_sub_header.vue';
import { createApprovers } from '../../mock_data'; import { createApprovers } from '../../mock_data';
...@@ -12,8 +13,7 @@ describe('DrawerAvatarsList component', () => { ...@@ -12,8 +13,7 @@ describe('DrawerAvatarsList component', () => {
const findHeader = () => wrapper.findComponent(DrawerSectionSubHeader); const findHeader = () => wrapper.findComponent(DrawerSectionSubHeader);
const findInlineAvatars = () => wrapper.findComponent(GlAvatarsInline); const findInlineAvatars = () => wrapper.findComponent(GlAvatarsInline);
const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink); const findAvatars = () => wrapper.findAllComponents(UserAvatar);
const findAvatars = () => wrapper.findAllComponents(GlAvatar);
const createComponent = (mountFn = shallowMount, propsData = {}) => { const createComponent = (mountFn = shallowMount, propsData = {}) => {
return mountFn(DrawerAvatarsList, { return mountFn(DrawerAvatarsList, {
...@@ -57,27 +57,16 @@ describe('DrawerAvatarsList component', () => { ...@@ -57,27 +57,16 @@ describe('DrawerAvatarsList component', () => {
}); });
it('renders the avatars', () => { it('renders the avatars', () => {
expect(findAvatarLinks()).toHaveLength(avatars.length); expect(findAvatars()).toHaveLength(avatars.length);
expect(findInlineAvatars().props()).toMatchObject({ expect(findInlineAvatars().props()).toMatchObject({
avatars, avatars,
badgeTooltipProp: 'name', badgeTooltipProp: 'name',
}); });
}); });
it('sets the correct attributes to the avatar links', () => {
expect(findAvatarLinks().at(0).classes()).toContain('js-user-link');
expect(findAvatarLinks().at(0).attributes()).toMatchObject({
title: avatars[0].name,
href: avatars[0].web_url,
'data-name': avatars[0].name,
'data-user-id': `${avatars[0].id}`,
});
});
it('sets the correct props to the avatars', () => { it('sets the correct props to the avatars', () => {
expect(findAvatars().at(0).props()).toMatchObject({ avatars.forEach((avatar, idx) => {
entityName: avatars[0].name, expect(findAvatars().at(idx).props('user')).toBe(avatar);
src: avatars[0].avatar_url,
}); });
}); });
}); });
......
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import UserAvatar from 'ee/compliance_dashboard/components/shared/user_avatar.vue';
import { DRAWER_AVATAR_SIZE } from 'ee/compliance_dashboard/constants';
import { createUser } from '../../mock_data';
describe('UserAvatar component', () => {
let wrapper;
const user = convertObjectPropsToCamelCase(createUser(1));
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const createComponent = (props = {}) => {
wrapper = shallowMount(UserAvatar, {
propsData: {
user,
...props,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('sets the correct attributes to the avatar', () => {
expect(findAvatar().props()).toMatchObject({
src: user.avatarUrl,
entityName: user.name,
size: DRAWER_AVATAR_SIZE,
});
});
it('sets the correct props to the avatar link', () => {
expect(findAvatarLink().attributes()).toMatchObject({
title: user.name,
href: user.webUrl,
'data-name': user.name,
'data-user-id': `${user.id}`,
});
});
});
import { shallowMount } from '@vue/test-utils';
import ViolationReason from 'ee/compliance_dashboard/components/violations/reason.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import UserAvatar from 'ee/compliance_dashboard/components/shared/user_avatar.vue';
import {
MERGE_REQUEST_VIOLATION_MESSAGES,
MERGE_REQUEST_VIOLATION_REASONS,
} from 'ee/compliance_dashboard/constants';
import { createUser } from '../../mock_data';
describe('ViolationReason component', () => {
let wrapper;
const user = convertObjectPropsToCamelCase(createUser(1));
const getViolationMessage = (reason) =>
MERGE_REQUEST_VIOLATION_MESSAGES[MERGE_REQUEST_VIOLATION_REASONS[reason]];
const findAvatar = () => wrapper.findComponent(UserAvatar);
const createComponent = (propsData = {}) => {
wrapper = shallowMount(ViolationReason, { propsData });
};
afterEach(() => {
wrapper.destroy();
});
describe('violation message', () => {
it.each`
reason | message
${0} | ${getViolationMessage(0)}
${1} | ${getViolationMessage(1)}
${2} | ${getViolationMessage(2)}
`(
'renders the violation message "$message" for the reason code $reason',
({ reason, message }) => {
createComponent({ reason });
expect(wrapper.text()).toContain(message);
},
);
});
describe('violation user', () => {
it('does not render a user avatar by default', () => {
createComponent({ reason: 0 });
expect(findAvatar().exists()).toBe(false);
});
it('renders a user avatar when the user prop is set', () => {
createComponent({ reason: 0, user });
expect(findAvatar().props('user')).toBe(user);
});
});
});
...@@ -8813,6 +8813,15 @@ msgstr "" ...@@ -8813,6 +8813,15 @@ msgstr ""
msgid "ComplianceFramework|New compliance framework" msgid "ComplianceFramework|New compliance framework"
msgstr "" msgstr ""
msgid "ComplianceReport|Approved by author"
msgstr ""
msgid "ComplianceReport|Approved by committer"
msgstr ""
msgid "ComplianceReport|Less than 2 approvers"
msgstr ""
msgid "Component" msgid "Component"
msgstr "" msgstr ""
......
...@@ -1040,4 +1040,15 @@ describe('common_utils', () => { ...@@ -1040,4 +1040,15 @@ describe('common_utils', () => {
expect(result).toEqual(['hello', 'helloWorld']); expect(result).toEqual(['hello', 'helloWorld']);
}); });
}); });
describe('convertArrayOfObjectsToCamelCase', () => {
it('returns a new array with snake_case object property names converted camelCase', () => {
const result = commonUtils.convertArrayOfObjectsToCamelCase([
{ hello: '' },
{ hello_world: '' },
]);
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
}); });
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