Commit 281eaa64 authored by Robert Hunt's avatar Robert Hunt

Set up the new approval status column

- Created the approval status component
- Added to the dashboard
- Updated the component tests to check the new component works
- Added translations
- Added changelog
parent 497620d5
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CiIcon,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
tooltip() {
return this.$options.tooltips[this.status];
},
iconName() {
return `status_${this.status}`;
},
iconStatus() {
const { status, iconName: icon } = this;
let group = status;
// Need to set this to be the group for warnings so the correct icon color fill is used
if (group === 'warning') {
group = 'success-with-warnings';
}
return {
group,
icon,
};
},
},
tooltips: {
success: s__('ApprovalStatusTooltip|Adheres to separation of duties'),
warning: s__('ApprovalStatusTooltip|At least one rule does not adhere to separation of duties'),
failed: s__('ApprovalStatusTooltip|Fails to adhere to separation of duties'),
},
};
</script>
<template>
<a
href="https://docs.gitlab.com/ee/user/compliance/compliance_dashboard/#approval-status-and-separation-of-duties"
>
<ci-icon v-gl-tooltip.left="tooltip" class="gl-display-flex" :status="iconStatus" />
</a>
</template>
...@@ -5,6 +5,7 @@ import { isEmpty } from 'lodash'; ...@@ -5,6 +5,7 @@ import { isEmpty } from 'lodash';
import { sprintf, __, s__ } from '~/locale'; import { sprintf, __, s__ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import ApprovalStatus from './approval_status.vue';
import Approvers from './approvers.vue'; import Approvers from './approvers.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MergeRequest from './merge_request.vue'; import MergeRequest from './merge_request.vue';
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { components: {
ApprovalStatus,
Approvers, Approvers,
EmptyState, EmptyState,
GridColumnHeading, GridColumnHeading,
...@@ -58,7 +60,7 @@ export default { ...@@ -58,7 +60,7 @@ export default {
timeTooltip(mergedAt) { timeTooltip(mergedAt) {
return this.tooltipTitle(mergedAt); return this.tooltipTitle(mergedAt);
}, },
hasPipeline(status) { hasStatus(status) {
return !isEmpty(status); return !isEmpty(status);
}, },
}, },
...@@ -66,6 +68,7 @@ export default { ...@@ -66,6 +68,7 @@ export default {
heading: __('Compliance Dashboard'), heading: __('Compliance Dashboard'),
subheading: __('Here you will find recent merge request activity'), subheading: __('Here you will find recent merge request activity'),
mergeRequestLabel: __('Merge Request'), mergeRequestLabel: __('Merge Request'),
approvalStatusLabel: __('Approval Status'),
pipelineStatusLabel: __('Pipeline'), pipelineStatusLabel: __('Pipeline'),
updatesLabel: __('Updates'), updatesLabel: __('Updates'),
}, },
...@@ -80,18 +83,28 @@ export default { ...@@ -80,18 +83,28 @@ export default {
</header> </header>
<div class="dashboard-grid"> <div class="dashboard-grid">
<grid-column-heading :heading="$options.strings.mergeRequestLabel" /> <grid-column-heading :heading="$options.strings.mergeRequestLabel" />
<grid-column-heading :heading="$options.strings.approvalStatusLabel" class="gl-text-center" />
<grid-column-heading :heading="$options.strings.pipelineStatusLabel" class="gl-text-center" /> <grid-column-heading :heading="$options.strings.pipelineStatusLabel" class="gl-text-center" />
<grid-column-heading :heading="$options.strings.updatesLabel" class="gl-text-right" /> <grid-column-heading :heading="$options.strings.updatesLabel" class="gl-text-right" />
<template v-for="mergeRequest in mergeRequests"> <template v-for="mergeRequest in mergeRequests">
<merge-request :key="key(mergeRequest.id, 'MR')" :merge-request="mergeRequest" /> <merge-request :key="key(mergeRequest.id, 'MR')" :merge-request="mergeRequest" />
<div
:key="key(mergeRequest.id, 'approvalStatus')"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<approval-status
v-if="hasStatus(mergeRequest.approval_status)"
:status="mergeRequest.approval_status"
/>
</div>
<div <div
:key="key(mergeRequest.id, 'pipeline')" :key="key(mergeRequest.id, 'pipeline')"
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5" class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
> >
<pipeline-status <pipeline-status
v-if="hasPipeline(mergeRequest.pipeline_status)" v-if="hasStatus(mergeRequest.pipeline_status)"
:status="mergeRequest.pipeline_status" :status="mergeRequest.pipeline_status"
/> />
</div> </div>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto auto;
grid-template-rows: auto; grid-template-rows: auto;
} }
......
---
title: Add MR approval settings column to the compliance dashboard
merge_request: 36589
author:
type: added
...@@ -23,6 +23,11 @@ exports[`ComplianceDashboard component when there are merge requests matches the ...@@ -23,6 +23,11 @@ exports[`ComplianceDashboard component when there are merge requests matches the
heading="Merge Request" heading="Merge Request"
/> />
<grid-column-heading-stub
class="gl-text-center"
heading="Approval Status"
/>
<grid-column-heading-stub <grid-column-heading-stub
class="gl-text-center" class="gl-text-center"
heading="Pipeline" heading="Pipeline"
...@@ -39,6 +44,12 @@ exports[`ComplianceDashboard component when there are merge requests matches the ...@@ -39,6 +44,12 @@ exports[`ComplianceDashboard component when there are merge requests matches the
Merge request 0 Merge request 0
</div> </div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div <div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5" class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
> >
...@@ -66,6 +77,12 @@ exports[`ComplianceDashboard component when there are merge requests matches the ...@@ -66,6 +77,12 @@ exports[`ComplianceDashboard component when there are merge requests matches the
Merge request 1 Merge request 1
</div> </div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div <div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5" class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
> >
......
import { shallowMount } from '@vue/test-utils';
import ApprovalStatus from 'ee/compliance_dashboard/components/approval_status.vue';
describe('ApprovalStatus component', () => {
let wrapper;
const findIcon = () => wrapper.find('.ci-icon');
const findLink = () => wrapper.find('a');
const createComponent = status => {
return shallowMount(ApprovalStatus, {
propsData: { status },
stubs: {
CiIcon: {
props: { status: Object },
template: `<div class="ci-icon">{{ status.icon }}</div>`,
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('with an approval status', () => {
const approvalStatus = 'success';
beforeEach(() => {
wrapper = createComponent(approvalStatus);
});
it('links to the approval status', () => {
expect(findLink().attributes('href')).toEqual(
'https://docs.gitlab.com/ee/user/compliance/compliance_dashboard/#approval-status-and-separation-of-duties',
);
});
it('renders an icon with the approval status', () => {
expect(findIcon().text()).toEqual(`status_${approvalStatus}`);
});
it.each`
status | tooltip
${'success'} | ${'Adheres to separation of duties'}
${'warning'} | ${'At least one rule does not adhere to separation of duties'}
${'failed'} | ${'Fails to adhere to separation of duties'}
`('shows the correct tooltip for $status', ({ status, tooltip }) => {
wrapper = createComponent(status);
expect(wrapper.vm.tooltip).toEqual(tooltip);
});
});
describe('with a warning approval status', () => {
const approvalStatus = 'warning';
beforeEach(() => {
wrapper = createComponent(approvalStatus);
});
it('returns the correct status object`', () => {
expect(wrapper.vm.iconStatus).toEqual({
group: 'success-with-warnings',
icon: 'status_warning',
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue'; import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue';
import ApprovalStatus from 'ee/compliance_dashboard/components/approval_status.vue';
import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue'; import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue';
import Approvers from 'ee/compliance_dashboard/components/approvers.vue'; import Approvers from 'ee/compliance_dashboard/components/approvers.vue';
import { createMergeRequests } from '../mock_data'; import { createMergeRequests } from '../mock_data';
...@@ -10,13 +11,14 @@ describe('ComplianceDashboard component', () => { ...@@ -10,13 +11,14 @@ describe('ComplianceDashboard component', () => {
const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]'); const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]');
const findTime = () => wrapper.find('time'); const findTime = () => wrapper.find('time');
const findApprovalStatus = () => wrapper.find(ApprovalStatus);
const findPipelineStatus = () => wrapper.find(PipelineStatus); const findPipelineStatus = () => wrapper.find(PipelineStatus);
const findApprovers = () => wrapper.find(Approvers); const findApprovers = () => wrapper.find(Approvers);
const createComponent = (props = {}, addPipeline = false) => { const createComponent = (props = {}, options = {}) => {
return shallowMount(ComplianceDashboard, { return shallowMount(ComplianceDashboard, {
propsData: { propsData: {
mergeRequests: createMergeRequests({ count: 2, addPipeline }), mergeRequests: createMergeRequests({ count: 2, options }),
isLastPage: false, isLastPage: false,
emptyStateSvgPath: 'empty.svg', emptyStateSvgPath: 'empty.svg',
...props, ...props,
...@@ -47,13 +49,24 @@ describe('ComplianceDashboard component', () => { ...@@ -47,13 +49,24 @@ describe('ComplianceDashboard component', () => {
expect(findMergeRequests().length).toEqual(2); expect(findMergeRequests().length).toEqual(2);
}); });
describe('approval status', () => {
it('does not render if there is no approval status', () => {
expect(findApprovalStatus().exists()).toBe(false);
});
it('renders if there is an approval status', () => {
wrapper = createComponent({}, { approvalStatus: 'success' });
expect(findApprovalStatus().exists()).toBe(true);
});
});
describe('pipeline status', () => { describe('pipeline status', () => {
it('does not render if there is no pipeline', () => { it('does not render if there is no pipeline', () => {
expect(findPipelineStatus().exists()).toBe(false); expect(findPipelineStatus().exists()).toBe(false);
}); });
it('renders if there is a pipeline', () => { it('renders if there is a pipeline', () => {
wrapper = createComponent({}, true); wrapper = createComponent({}, { addPipeline: true });
expect(findPipelineStatus().exists()).toBe(true); expect(findPipelineStatus().exists()).toBe(true);
}); });
}); });
......
...@@ -13,7 +13,7 @@ const createUser = id => ({ ...@@ -13,7 +13,7 @@ const createUser = id => ({
web_url: `http://localhost:3000/user-${id}`, web_url: `http://localhost:3000/user-${id}`,
}); });
export const createMergeRequest = ({ id = 1, pipeline, approvers } = {}) => { export const createMergeRequest = ({ id = 1, pipeline, approvers, approvalStatus } = {}) => {
const mergeRequest = { const mergeRequest = {
id, id,
approved_by_users: [], approved_by_users: [],
...@@ -29,9 +29,15 @@ export const createMergeRequest = ({ id = 1, pipeline, approvers } = {}) => { ...@@ -29,9 +29,15 @@ export const createMergeRequest = ({ id = 1, pipeline, approvers } = {}) => {
if (pipeline) { if (pipeline) {
mergeRequest.pipeline_status = pipeline; mergeRequest.pipeline_status = pipeline;
} }
if (approvers) { if (approvers) {
mergeRequest.approved_by_users = approvers; mergeRequest.approved_by_users = approvers;
} }
if (approvalStatus) {
mergeRequest.approval_status = approvalStatus;
}
return mergeRequest; return mergeRequest;
}; };
...@@ -53,10 +59,14 @@ export const createApprovers = count => { ...@@ -53,10 +59,14 @@ export const createApprovers = count => {
.map((_, id) => createUser(id)); .map((_, id) => createUser(id));
}; };
export const createMergeRequests = ({ count = 1, addPipeline = false } = {}) => { export const createMergeRequests = ({ count = 1, options = {} } = {}) => {
return Array(count) return Array(count)
.fill() .fill()
.map((_, id) => .map((_, id) =>
createMergeRequest({ id, pipeline: addPipeline ? createPipelineStatus('success') : null }), createMergeRequest({
id,
approvalStatus: options.approvalStatus,
pipeline: options.addPipeline ? createPipelineStatus('success') : null,
}),
); );
}; };
...@@ -2937,6 +2937,9 @@ msgstr "" ...@@ -2937,6 +2937,9 @@ msgstr ""
msgid "Applying suggestions..." msgid "Applying suggestions..."
msgstr "" msgstr ""
msgid "Approval Status"
msgstr ""
msgid "Approval rules" msgid "Approval rules"
msgstr "" msgstr ""
...@@ -2984,6 +2987,15 @@ msgstr "" ...@@ -2984,6 +2987,15 @@ msgstr ""
msgid "ApprovalRule|e.g. QA, Security, etc." msgid "ApprovalRule|e.g. QA, Security, etc."
msgstr "" msgstr ""
msgid "ApprovalStatusTooltip|Adheres to separation of duties"
msgstr ""
msgid "ApprovalStatusTooltip|At least one rule does not adhere to separation of duties"
msgstr ""
msgid "ApprovalStatusTooltip|Fails to adhere to separation of duties"
msgstr ""
msgid "Approvals|Section: %section" msgid "Approvals|Section: %section"
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