Commit bacc37c2 authored by Robert Hunt's avatar Robert Hunt

Setup compliance dashboard merge request drawer

- Created new development feature flag
- Created basic drawer component
- Added draw to dashboard
- Wrapped MR item with a link which triggers the opening/closing of the
drawer
- Added new CSS to handle the fact that the MR item needs to span the
full row but also still maintain the same column widths throughout
- Removed unnecessary heading class
- Updated tests for when the feature is enabled and disabled
parent 398ddd20
......@@ -2,7 +2,9 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { COMPLIANCE_TAB_COOKIE_KEY } from '../constants';
import MergeRequestDrawer from './drawer.vue';
import EmptyState from './empty_state.vue';
import MergeRequestsGrid from './merge_requests/grid.vue';
import MergeCommitsExportButton from './merge_requests/merge_commits_export_button.vue';
......@@ -10,12 +12,14 @@ import MergeCommitsExportButton from './merge_requests/merge_commits_export_butt
export default {
name: 'ComplianceDashboard',
components: {
MergeRequestDrawer,
MergeRequestsGrid,
EmptyState,
GlTab,
GlTabs,
MergeCommitsExportButton,
},
mixins: [glFeatureFlagsMixin()],
props: {
emptyStateSvgPath: {
type: String,
......@@ -36,6 +40,12 @@ export default {
default: '',
},
},
data() {
return {
showDrawer: false,
drawerData: {},
};
},
computed: {
hasMergeRequests() {
return this.mergeRequests.length > 0;
......@@ -43,11 +53,29 @@ export default {
hasMergeCommitsCsvExportPath() {
return this.mergeCommitsCsvExportPath !== '';
},
drawerEnabled() {
return this.glFeatures.complianceDashboardDrawer;
},
},
methods: {
showTabs() {
return Cookies.get(COMPLIANCE_TAB_COOKIE_KEY) === 'true';
},
toggleDrawer(mergeRequest) {
if (this.showDrawer && mergeRequest.id === this.drawerData.id) {
this.closeDrawer();
} else {
this.openDrawer(mergeRequest);
}
},
openDrawer(mergeRequest) {
this.showDrawer = true;
this.drawerData = mergeRequest;
},
closeDrawer() {
this.showDrawer = false;
this.drawerData = {};
},
},
strings: {
heading: __('Compliance Dashboard'),
......@@ -75,10 +103,27 @@ export default {
<template #title>
<span>{{ $options.strings.mergeRequestsTabLabel }}</span>
</template>
<merge-requests-grid :merge-requests="mergeRequests" :is-last-page="isLastPage" />
<merge-requests-grid
:merge-requests="mergeRequests"
:is-last-page="isLastPage"
@toggleDrawer="toggleDrawer"
/>
</gl-tab>
</gl-tabs>
<merge-requests-grid v-else :merge-requests="mergeRequests" :is-last-page="isLastPage" />
<merge-requests-grid
v-else
:merge-requests="mergeRequests"
:is-last-page="isLastPage"
:drawer-enabled="drawerEnabled"
@toggleDrawer="toggleDrawer"
/>
<merge-request-drawer
v-if="drawerEnabled"
:show-drawer="showDrawer"
:merge-request="drawerData"
z-index="252"
@close="closeDrawer"
/>
</div>
<empty-state v-else :image-path="emptyStateSvgPath" />
</template>
<script>
import { GlDrawer } from '@gitlab/ui';
export default {
components: {
GlDrawer,
},
props: {
mergeRequest: {
type: Object,
required: true,
},
showDrawer: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
if (wrapperEl) {
return `${wrapperEl.offsetTop}px`;
}
return '';
},
},
// We set the drawer's z-index to 252 to clear flash messages that might be displayed in the page
// and that have a z-index of 251.
Z_INDEX: 252,
};
</script>
<template>
<gl-drawer
:open="showDrawer"
:header-height="getDrawerHeaderHeight()"
:z-index="$options.Z_INDEX"
@close="$emit('close')"
>
<template #header>
<h4 data-testid="dashboard-drawer-title">{{ mergeRequest.title }}</h4>
</template>
</gl-drawer>
</template>
......@@ -29,7 +29,7 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-justify-content-end">
<div class="gl-display-flex gl-align-items-center gl-justify-content-end gl-text-gray-900">
<gl-sprintf :message="this.$options.strings.branchDetails">
<template #sourceBranch>
<span class="gl-mr-2 gl-min-w-0">
......
......@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
drawerEnabled: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
key(id, value) {
......@@ -65,6 +70,61 @@ export default {
<grid-column-heading :heading="$options.strings.pipelineStatusLabel" class="gl-text-center" />
<grid-column-heading :heading="$options.strings.updatesLabel" class="gl-text-right" />
<!-- TODO: Remove the if/else and duplicate components with https://gitlab.com/gitlab-org/gitlab/-/issues/334682 -->
<template v-if="drawerEnabled">
<a
v-for="mergeRequest in mergeRequests"
:key="mergeRequest.id"
class="dashboard-merge-request dashboard-grid gl-display-grid gl-grid-tpl-rows-auto gl-hover-bg-blue-50 gl-hover-text-decoration-none gl-hover-cursor-pointer"
data-testid="merge-request-link"
@click="$emit('toggleDrawer', mergeRequest)"
>
<merge-request
:key="key(mergeRequest.id, $options.keyTypes.mergeRequest)"
:merge-request="mergeRequest"
/>
<status
:key="key(mergeRequest.id, 'approval')"
:status="{ type: 'approval', data: mergeRequest.approval_status }"
/>
<status
:key="key(mergeRequest.id, 'pipeline')"
:status="{ type: 'pipeline', data: mergeRequest.pipeline_status }"
/>
<div
:key="key(mergeRequest.id, $options.keyTypes.updates)"
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers :approvers="mergeRequest.approved_by_users" />
<branch-details
v-if="hasBranchDetails(mergeRequest)"
:source-branch="{
name: mergeRequest.source_branch,
uri: mergeRequest.source_branch_uri,
}"
:target-branch="{
name: mergeRequest.target_branch,
uri: mergeRequest.target_branch_uri,
}"
/>
<time-ago-tooltip
:time="mergeRequest.merged_at"
tooltip-placement="bottom"
class="gl-text-gray-500"
>
<template #default="{ timeAgo }">
<gl-sprintf :message="$options.strings.mergedAtText">
<template #timeAgo>{{ timeAgo }}</template>
</gl-sprintf>
</template>
</time-ago-tooltip>
</div>
</a>
</template>
<template v-else>
<template v-for="mergeRequest in mergeRequests">
<merge-request
:key="key(mergeRequest.id, $options.keyTypes.mergeRequest)"
......@@ -110,6 +170,7 @@ export default {
</time-ago-tooltip>
</div>
</template>
</template>
</div>
<pagination class="gl-mt-5" :is-last-page="isLastPage" />
......
......@@ -32,9 +32,11 @@ export default {
class="gl-grid-col-start-1 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
data-testid="merge-request"
>
<a :href="mergeRequest.path" class="gl-display-block gl-text-gray-900 gl-font-weight-bold">
<div>
<a :href="mergeRequest.path" class="gl-text-gray-900 gl-font-weight-bold">
{{ mergeRequest.title }}
</a>
</div>
<span class="gl-text-gray-500">{{ mergeRequest.issuable_reference }}</span>
<span class="issuable-authored gl-text-gray-500 gl-display-inline-flex gl-align-items-center">
- {{ $options.strings.createdBy }}
......
......@@ -10,9 +10,7 @@ export default {
</script>
<template>
<p
class="grid-column-heading gl-text-gray-500 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100 gl-mb-0 gl-p-5"
>
<p class="gl-text-gray-500 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100 gl-mb-0 gl-p-5">
{{ heading }}
</p>
</template>
......@@ -3,6 +3,11 @@
min-width: 550px;
.dashboard-grid {
grid-template-columns: 1fr auto auto 35%;
// Column widths: Merge request | approval status | pipeline status | updates
grid-template-columns: 1fr 12% 8% 35%;
}
.dashboard-merge-request {
grid-column: auto / span 4;
}
}
......@@ -6,6 +6,9 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont
layout 'group'
before_action :authorize_compliance_dashboard!
before_action do
push_frontend_feature_flag(:compliance_dashboard_drawer, @group, default_enabled: :yaml)
end
track_unique_visits :show, target_id: 'g_compliance_dashboard'
......
---
name: compliance_dashboard_drawer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64771
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334682
milestone: '14.1'
type: development
group: group::compliance
default_enabled: false
......@@ -47,6 +47,8 @@ exports[`ComplianceDashboard component when there are merge requests and the sho
</template>
</b-tab-stub>
</gl-tabs-stub>
<!---->
</div>
`;
......@@ -77,6 +79,8 @@ exports[`ComplianceDashboard component when there are merge requests matches the
<merge-requests-grid-stub
mergerequests="[object Object],[object Object]"
/>
<!---->
</div>
`;
......
......@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue';
import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue';
import MergeRequestGrid from 'ee/compliance_dashboard/components/merge_requests/grid.vue';
import MergeCommitsExportButton from 'ee/compliance_dashboard/components/merge_requests/merge_commits_export_button.vue';
import { COMPLIANCE_TAB_COOKIE_KEY } from 'ee/compliance_dashboard/constants';
......@@ -15,11 +16,12 @@ describe('ComplianceDashboard component', () => {
const mergeRequests = createMergeRequests({ count: 2 });
const mergeCommitsCsvExportPath = '/csv';
const findMergeRequestsGrid = () => wrapper.find(MergeRequestGrid);
const findMergeCommitsExportButton = () => wrapper.find(MergeCommitsExportButton);
const findDashboardTabs = () => wrapper.find(GlTabs);
const findMergeRequestsGrid = () => wrapper.findComponent(MergeRequestGrid);
const findMergeRequestsDrawer = () => wrapper.findComponent(MergeRequestDrawer);
const findMergeCommitsExportButton = () => wrapper.findComponent(MergeCommitsExportButton);
const findDashboardTabs = () => wrapper.findComponent(GlTabs);
const createComponent = (props = {}) => {
const createComponent = (props = {}, glFeatures = {}) => {
return shallowMount(ComplianceDashboard, {
propsData: {
mergeRequests,
......@@ -28,6 +30,9 @@ describe('ComplianceDashboard component', () => {
emptyStateSvgPath: 'empty.svg',
...props,
},
provide: {
glFeatures,
},
stubs: {
GlTab,
},
......@@ -106,4 +111,57 @@ describe('ComplianceDashboard component', () => {
});
});
});
describe('with the merge requests drawer', () => {
describe('when the feature is enabled', () => {
beforeEach(() => {
wrapper = createComponent({}, { complianceDashboardDrawer: true });
});
it('opens the drawer', async () => {
await findMergeRequestsGrid().vm.$emit('toggleDrawer', mergeRequests[0]);
expect(findMergeRequestsGrid().props('drawerEnabled')).toBe(true);
expect(findMergeRequestsDrawer().props('showDrawer')).toBe(true);
expect(findMergeRequestsDrawer().props('mergeRequest')).toStrictEqual(mergeRequests[0]);
});
it('closes the drawer via the drawer close event', async () => {
await findMergeRequestsDrawer().vm.$emit('close');
expect(findMergeRequestsGrid().props('drawerEnabled')).toBe(true);
expect(findMergeRequestsDrawer().props('showDrawer')).toBe(false);
expect(findMergeRequestsDrawer().props('mergeRequest')).toEqual({});
});
it('closes the drawer via the grid toggle event', async () => {
await findMergeRequestsGrid().vm.$emit('toggleDrawer', mergeRequests[0]);
await findMergeRequestsGrid().vm.$emit('toggleDrawer', mergeRequests[0]);
expect(findMergeRequestsGrid().props('drawerEnabled')).toBe(true);
expect(findMergeRequestsDrawer().props('showDrawer')).toBe(false);
expect(findMergeRequestsDrawer().props('mergeRequest')).toEqual({});
});
it('swaps the drawer when a new merge request is selected', async () => {
await findMergeRequestsGrid().vm.$emit('toggleDrawer', mergeRequests[0]);
await findMergeRequestsGrid().vm.$emit('toggleDrawer', mergeRequests[1]);
expect(findMergeRequestsGrid().props('drawerEnabled')).toBe(true);
expect(findMergeRequestsDrawer().props('showDrawer')).toBe(true);
expect(findMergeRequestsDrawer().props('mergeRequest')).toStrictEqual(mergeRequests[1]);
});
});
describe('when the feature is disabled', () => {
beforeEach(() => {
wrapper = createComponent({}, { complianceDashboardDrawer: false });
});
it('does not show the drawer', () => {
expect(findMergeRequestsGrid().props('drawerEnabled')).toBe(false);
expect(findMergeRequestsDrawer().exists()).toBe(false);
});
});
});
});
import { GlDrawer } from '@gitlab/ui';
import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMergeRequests } from '../mock_data';
describe('MergeRequestDrawer component', () => {
let wrapper;
const mergeRequest = createMergeRequests({ count: 1 })[0];
const findTitle = () => wrapper.findByTestId('dashboard-drawer-title');
const findDrawer = () => wrapper.findComponent(GlDrawer);
const createComponent = (props) => {
return shallowMountExtended(MergeRequestDrawer, {
propsData: {
mergeRequest,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when closed', () => {
beforeEach(() => {
wrapper = createComponent({ showDrawer: false });
});
it('the drawer is not shown', () => {
expect(findDrawer().props('open')).toBe(false);
});
});
describe('when open', () => {
beforeEach(() => {
wrapper = createComponent({ showDrawer: true });
});
it('the drawer is shown', () => {
expect(findDrawer().props('open')).toBe(true);
});
it('has the drawer title', () => {
expect(findTitle().text()).toEqual(mergeRequest.title);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MergeRequestsGrid component when intialized matches the snapshot 1`] = `
exports[`MergeRequestsGrid component when drawer enabled is false when initialized matches the snapshot 1`] = `
<div>
<div
class="dashboard-grid gl-display-grid gl-grid-tpl-rows-auto"
......@@ -91,3 +91,105 @@ exports[`MergeRequestsGrid component when intialized matches the snapshot 1`] =
/>
</div>
`;
exports[`MergeRequestsGrid component when drawer enabled is true when initialized matches the snapshot 1`] = `
<div>
<div
class="dashboard-grid gl-display-grid gl-grid-tpl-rows-auto"
>
<grid-column-heading-stub
heading="Merge Request"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Approval Status"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Pipeline"
/>
<grid-column-heading-stub
class="gl-text-right"
heading="Updates"
/>
<a
class="dashboard-merge-request dashboard-grid gl-display-grid gl-grid-tpl-rows-auto gl-hover-bg-blue-50 gl-hover-text-decoration-none gl-hover-cursor-pointer"
data-testid="merge-request-link"
>
<div
data-testid="merge-request"
>
Merge request 0
</div>
<status-stub
status="[object Object]"
/>
<status-stub
status="[object Object]"
/>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<!---->
<time-ago-tooltip-stub
class="gl-text-gray-500"
cssclass=""
time="2020-01-01T00:00:00.000Z"
tooltipplacement="bottom"
/>
</div>
</a>
<a
class="dashboard-merge-request dashboard-grid gl-display-grid gl-grid-tpl-rows-auto gl-hover-bg-blue-50 gl-hover-text-decoration-none gl-hover-cursor-pointer"
data-testid="merge-request-link"
>
<div
data-testid="merge-request"
>
Merge request 1
</div>
<status-stub
status="[object Object]"
/>
<status-stub
status="[object Object]"
/>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<!---->
<time-ago-tooltip-stub
class="gl-text-gray-500"
cssclass=""
time="2020-01-01T00:00:00.000Z"
tooltipplacement="bottom"
/>
</div>
</a>
</div>
<pagination-stub
class="gl-mt-5"
/>
</div>
`;
......@@ -5,14 +5,16 @@ exports[`MergeRequest component when there is a merge request matches the snapsh
class="gl-grid-col-start-1 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
data-testid="merge-request"
>
<div>
<a
class="gl-display-block gl-text-gray-900 gl-font-weight-bold"
class="gl-text-gray-900 gl-font-weight-bold"
href="/h5bp/html5-boilerplate/-/merge_requests/1"
>
Merge request 1
</a>
</div>
<span
class="gl-text-gray-500"
......
import { shallowMount } from '@vue/test-utils';
import Approvers from 'ee/compliance_dashboard/components/merge_requests/approvers.vue';
import BranchDetails from 'ee/compliance_dashboard/components/merge_requests/branch_details.vue';
import MergeRequestsGrid from 'ee/compliance_dashboard/components/merge_requests/grid.vue';
import Status from 'ee/compliance_dashboard/components/merge_requests/status.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { createMergeRequests, mergedAt } from '../../mock_data';
describe('MergeRequestsGrid component', () => {
let wrapper;
const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]');
const findTime = () => wrapper.find(TimeAgoTooltip);
const findStatuses = () => wrapper.findAll(Status);
const findApprovers = () => wrapper.find(Approvers);
const findBranchDetails = () => wrapper.find(BranchDetails);
const findMergeRequestLinks = () => wrapper.findAllByTestId('merge-request-link');
const findMergeRequests = () => wrapper.findAllByTestId('merge-request');
const findTime = () => wrapper.findComponent(TimeAgoTooltip);
const findStatuses = () => wrapper.findAllComponents(Status);
const findApprovers = () => wrapper.findComponent(Approvers);
const findBranchDetails = () => wrapper.findComponent(BranchDetails);
const createComponent = (mergeRequests = {}) => {
return shallowMount(MergeRequestsGrid, {
const createComponent = (mergeRequests = {}, drawerEnabled = false) => {
return shallowMountExtended(MergeRequestsGrid, {
propsData: {
mergeRequests,
isLastPage: false,
drawerEnabled,
},
stubs: {
MergeRequest: {
......@@ -36,9 +36,11 @@ describe('MergeRequestsGrid component', () => {
wrapper.destroy();
});
describe('when intialized', () => {
// TODO: Remove the each with https://gitlab.com/gitlab-org/gitlab/-/issues/334682
describe.each([true, false])('when drawer enabled is %s', (drawerEnabled) => {
describe('when initialized', () => {
beforeEach(() => {
wrapper = createComponent(createMergeRequests({ count: 2, props: {} }));
wrapper = createComponent(createMergeRequests({ count: 2, props: {} }), drawerEnabled);
});
it('matches the snapshot', () => {
......@@ -49,10 +51,23 @@ describe('MergeRequestsGrid component', () => {
expect(findMergeRequests()).toHaveLength(2);
});
it('passes the correct props to the statuses', () => {
it('renders the approvers list', () => {
expect(findApprovers().exists()).toBe(true);
});
it('renders the "merged at" time', () => {
expect(findTime().props('time')).toEqual(mergedAt());
});
});
describe('statuses', () => {
const mergeRequest = createMergeRequests({ count: 1 });
wrapper = createComponent(mergeRequest);
beforeEach(() => {
wrapper = createComponent(mergeRequest, drawerEnabled);
});
it('passes the correct props to the statuses', () => {
findStatuses().wrappers.forEach((status) => {
const { type, data } = status.props('status');
......@@ -70,9 +85,12 @@ describe('MergeRequestsGrid component', () => {
}
});
});
});
describe('branch details', () => {
it('does not render if there are no branch details', () => {
wrapper = createComponent(createMergeRequests({ count: 2, props: {} }), drawerEnabled);
expect(findBranchDetails().exists()).toBe(false);
});
......@@ -82,17 +100,26 @@ describe('MergeRequestsGrid component', () => {
count: 2,
props: { target_branch: 'main', source_branch: 'feature' },
}),
drawerEnabled,
);
expect(findBranchDetails().exists()).toBe(true);
});
});
});
it('renders the approvers list', () => {
expect(findApprovers().exists()).toBe(true);
describe('when the drawer is enabled', () => {
const mergeRequests = createMergeRequests({ count: 2, props: {} });
beforeEach(() => {
const mergeRequest = createMergeRequests({ count: 1 });
wrapper = createComponent(mergeRequest, true);
});
it('renders the "merged at" time', () => {
expect(findTime().props('time')).toEqual(mergedAt());
it('toggles the drawer when a merge request link is clicked', () => {
findMergeRequestLinks().at(0).trigger('click');
expect(wrapper.emitted('toggleDrawer')[0][0]).toStrictEqual(mergeRequests[0]);
});
});
});
......@@ -2,7 +2,7 @@
exports[`GridColumnHeading component behaviour matches the screenshot 1`] = `
<p
class="grid-column-heading gl-text-gray-500 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100 gl-mb-0 gl-p-5"
class="gl-text-gray-500 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100 gl-mb-0 gl-p-5"
>
Test heading
......
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