Commit 5dcaa143 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'mw-productivity-analytics-mr-table' into 'master'

Add MR table component for Productivity Analytics

See merge request gitlab-org/gitlab-ee!14757
parents 40fb30f2 c7530356
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import MergeRequestTableRow from './mr_table_row.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
MergeRequestTableRow,
},
props: {
mergeRequests: {
type: Array,
required: true,
},
columnOptions: {
type: Object,
required: true,
},
metricType: {
type: String,
required: true,
},
metricLabel: {
type: String,
required: true,
},
},
computed: {
metricDropdownLabel() {
return this.columnOptions[this.metricType];
},
},
};
</script>
<template>
<div class="mr-table">
<div class="card">
<div class="card-header border-bottom-0">
<div role="row" class="gl-responsive-table-row table-row-header d-flex py-0">
<div role="rowheader" class="table-section section-50 d-none d-md-flex">
{{ __('Title') }}
</div>
<div role="rowheader" class="table-section section-50">
<div class="d-flex">
<span class="d-none d-md-flex metric-col">{{ __('Time to merge') }}</span>
<gl-dropdown
class="w-100 metric-col"
toggle-class="dropdown-menu-toggle w-100"
menu-class="w-100 mw-100"
:text="metricDropdownLabel"
>
<gl-dropdown-item
v-for="(value, key) in columnOptions"
:key="key"
active-class="is-active"
class="w-100"
@click="$emit('columnMetricChange', key)"
>
{{ value }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
</div>
</div>
<div class="card-body py-0">
<merge-request-table-row
v-for="model in mergeRequests"
:key="model.id"
:merge-request="model"
:metric-type="metricType"
:metric-label="metricLabel"
/>
</div>
</div>
</div>
</template>
<script>
import { sprintf, __, n__ } from '~/locale';
import { GlLink, GlAvatar } from '@gitlab/ui';
export default {
components: {
GlLink,
GlAvatar,
},
props: {
mergeRequest: {
type: Object,
required: true,
},
metricType: {
type: String,
required: true,
},
metricLabel: {
type: String,
required: true,
},
},
computed: {
mrId() {
return `!${this.mergeRequest.iid}`;
},
commitCount() {
return n__('%d commit', '%d commits', this.mergeRequest.commits_count);
},
locPerCommit() {
return sprintf(__('%{count} LOC/commit'), { count: this.mergeRequest.loc_per_commit });
},
filesTouched() {
return sprintf(__('%{count} files touched'), { count: this.mergeRequest.files_touched });
},
selectedMetric() {
return this.mergeRequest[this.metricType];
},
metricTimeUnit() {
return this.metricType === 'days_to_merge'
? n__('day', 'days', this.selectedMetric)
: n__('Time|hr', 'Time|hrs', this.selectedMetric);
},
},
};
</script>
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<div
class="table-section section-50 d-flex flex-row-reverse flex-md-row justify-content-between justify-content-md-start qa-mr-details"
>
<div>
<gl-avatar :src="mergeRequest.author_avatar_url" :size="16" />
</div>
<div class="mw-90p">
<h5 class="item-title mr-title my-0 mw-90p d-block str-truncated">
<gl-link :href="mergeRequest.merge_request_url" target="_blank">{{
mergeRequest.title
}}</gl-link>
</h5>
<ul class="horizontal-list list-items-separated text-secondary">
<li>{{ mrId }}</li>
<li>{{ commitCount }}</li>
<li>{{ locPerCommit }}</li>
<li>{{ filesTouched }}</li>
</ul>
</div>
</div>
<div class="table-section section-50 d-flex flex-row align-items-start qa-mr-metrics">
<div class="metric-col">
<span class="time">
{{ mergeRequest.time_to_merge }}
<span> {{ n__('Time|hr', 'Time|hrs', mergeRequest.time_to_merge) }} </span>
</span>
<span class="d-flex d-md-none text-secondary metric-label">{{ __('Time to merge') }}</span>
</div>
<div class="metric-col">
<span class="time">
{{ selectedMetric }}
<span> {{ metricTimeUnit }} </span>
</span>
<span class="d-flex d-md-none text-secondary metric-label">{{ metricLabel }}</span>
</div>
</div>
</div>
</template>
.mr-table {
@include media-breakpoint-down(md) {
.gl-responsive-table-row {
border: 0;
margin-top: 10px;
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
}
.table-section {
border: 0;
min-height: auto;
padding-left: 0;
padding-right: 0;
}
.metric-col {
flex: 1;
}
.metric-label {
white-space: pre-wrap;
}
.avatar {
margin-right: 0;
}
}
.metric-col {
flex: 0 0 50%;
.time {
font-size: $gl-font-size-large;
span {
font-size: $gl-font-size;
}
}
}
.card-header {
background-color: transparent;
font-weight: $gl-font-weight-bold;
}
.mr-title {
a {
color: $gl-text-color;
}
}
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MergeRequestTableRow component on creation matches the snapshot 1`] = `
<div
class="gl-responsive-table-row-layout gl-responsive-table-row"
>
<div
class="table-section section-50 d-flex flex-row-reverse flex-md-row justify-content-between justify-content-md-start qa-mr-details"
>
<div>
<glavatar-stub
alt="avatar"
entityid="0"
entityname=""
shape="circle"
size="16"
/>
</div>
<div
class="mw-90p"
>
<h5
class="item-title mr-title my-0 mw-90p d-block str-truncated"
>
<gllink-stub
href="http://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/10"
target="_blank"
>
This is a test MR
</gllink-stub>
</h5>
<ul
class="horizontal-list list-items-separated text-secondary"
>
<li>
!10
</li>
<li>
1 commit
</li>
<li>
3 LOC/commit
</li>
<li>
1 files touched
</li>
</ul>
</div>
</div>
<div
class="table-section section-50 d-flex flex-row align-items-start qa-mr-metrics"
>
<div
class="metric-col"
>
<span
class="time"
>
0
<span>
hrs
</span>
</span>
<span
class="d-flex d-md-none text-secondary metric-label"
>
Time to merge
</span>
</div>
<div
class="metric-col"
>
<span
class="time"
>
0
<span>
hrs
</span>
</span>
<span
class="d-flex d-md-none text-secondary metric-label"
>
Time from first comment to last commit
</span>
</div>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MergeRequestTable component matches the snapshot 1`] = `
<div
class="mr-table"
>
<div
class="card"
>
<div
class="card-header border-bottom-0"
>
<div
class="gl-responsive-table-row table-row-header d-flex py-0"
role="row"
>
<div
class="table-section section-50 d-none d-md-flex"
role="rowheader"
>
Title
</div>
<div
class="table-section section-50"
role="rowheader"
>
<div
class="d-flex"
>
<span
class="d-none d-md-flex metric-col"
>
Time to merge
</span>
<gldropdown-stub
class="w-100 metric-col"
menu-class="w-100 mw-100"
text="Time from first comment to last commit"
toggle-class="dropdown-menu-toggle w-100"
>
<gldropdownitem-stub
active-class="is-active"
class="w-100"
>
Time from first commit until first comment
</gldropdownitem-stub>
<gldropdownitem-stub
active-class="is-active"
class="w-100"
>
Time from first comment to last commit
</gldropdownitem-stub>
<gldropdownitem-stub
active-class="is-active"
class="w-100"
>
Time from last commit to merge
</gldropdownitem-stub>
</gldropdown-stub>
</div>
</div>
</div>
</div>
<div
class="card-body py-0"
>
<mergerequesttablerow-stub
mergerequest="[object Object]"
metriclabel="Time from first comment to last commit"
metrictype="time_to_last_commit"
/>
<mergerequesttablerow-stub
mergerequest="[object Object]"
metriclabel="Time from first comment to last commit"
metrictype="time_to_last_commit"
/>
</div>
</div>
</div>
`;
import { createLocalVue, shallowMount } from '@vue/test-utils';
import MergeRequestTableRow from 'ee/analytics/productivity_analytics/components/mr_table_row.vue';
import { GlAvatar } from '@gitlab/ui';
import mockMergeRequests from './../mock_data';
describe('MergeRequestTableRow component', () => {
let wrapper;
const defaultProps = {
mergeRequest: mockMergeRequests[0],
metricType: 'time_to_last_commit',
metricLabel: 'Time from first comment to last commit',
};
const factory = (props = defaultProps) => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MergeRequestTableRow), {
localVue,
sync: false,
propsData: { ...props },
});
};
const findMrDetails = () => wrapper.find('.qa-mr-details');
const findMrMetrics = () => wrapper.find('.qa-mr-metrics');
const findMetricColumns = () => findMrMetrics().findAll('.metric-col');
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
factory();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('template', () => {
beforeEach(() => {
factory();
});
it('renders the avatar with correct URL', () => {
const avatar = wrapper.find(GlAvatar);
expect(avatar.exists()).toBe(true);
expect(avatar.props('src')).toEqual(defaultProps.mergeRequest.author_avatar_url);
});
it('renders the merge request title', () => {
const title = findMrDetails().find('.mr-title');
expect(title.exists()).toBe(true);
expect(title.text()).toContain(defaultProps.mergeRequest.title);
});
describe('metric columns', () => {
it('renders two metric columns', () => {
expect(findMetricColumns().length).toBe(2);
});
it('renders the "Time to merge" metric column', () => {
expect(
findMetricColumns()
.at(0)
.text(),
).toContain(defaultProps.mergeRequest.time_to_merge);
});
});
});
describe('computed', () => {
describe('mrId', () => {
beforeEach(() => {
factory();
});
it('returns the merge request ID with a leading "!"', () => {
expect(wrapper.vm.mrId).toBe(`!${defaultProps.mergeRequest.iid}`);
});
});
describe('commitCount', () => {
beforeEach(() => {
factory();
});
it('returns the number of commits', () => {
expect(wrapper.vm.commitCount).toBe(`${defaultProps.mergeRequest.commits_count} commit`);
});
});
describe('locPerCommit', () => {
beforeEach(() => {
factory();
});
it('returns the LOC per commit', () => {
expect(wrapper.vm.locPerCommit).toBe(
`${defaultProps.mergeRequest.loc_per_commit} LOC/commit`,
);
});
});
describe('filesTouched', () => {
beforeEach(() => {
factory();
});
it('returns the number of files touched', () => {
expect(wrapper.vm.filesTouched).toBe(
`${defaultProps.mergeRequest.files_touched} files touched`,
);
});
});
describe('selectedMetric', () => {
beforeEach(() => {
factory();
});
it("returns the selected metric's key", () => {
expect(wrapper.vm.selectedMetric).toBe(defaultProps.mergeRequest[defaultProps.metricType]);
});
});
describe('metricTimeUnit', () => {
describe('when metricType is "days_to_merge"', () => {
beforeEach(() => {
factory({
...defaultProps,
metricType: 'days_to_merge',
});
});
it('returns "days"', () => {
expect(wrapper.vm.metricTimeUnit).toBe('days');
});
});
describe('when metricType is not "days_to_merge"', () => {
beforeEach(() => {
factory({
...defaultProps,
metricType: 'time_to_last_commit',
});
});
it('returns "hrs"', () => {
expect(wrapper.vm.metricTimeUnit).toBe('hrs');
});
});
});
});
});
import { createLocalVue, shallowMount } from '@vue/test-utils';
import MergeRequestTable from 'ee/analytics/productivity_analytics/components/mr_table.vue';
import MergeRequestTableRow from 'ee/analytics/productivity_analytics/components/mr_table_row.vue';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import mockMergeRequests from './../mock_data';
describe('MergeRequestTable component', () => {
let wrapper;
const defaultProps = {
mergeRequests: mockMergeRequests,
columnOptions: {
time_to_first_comment: 'Time from first commit until first comment',
time_to_last_commit: 'Time from first comment to last commit',
time_to_merge: 'Time from last commit to merge',
},
metricType: 'time_to_last_commit',
metricLabel: 'Time from first comment to last commit',
};
const factory = (props = defaultProps) => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MergeRequestTable), {
localVue,
sync: false,
propsData: { ...props },
});
};
const findMergeRequestTableRows = () => wrapper.findAll(MergeRequestTableRow);
const findTableHeader = () => wrapper.find('.table-row-header');
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('template', () => {
it('renders the table header and the column titles', () => {
expect(findTableHeader().exists()).toBe(true);
expect(findTableHeader().text()).toContain('Title');
expect(findTableHeader().text()).toContain('Time to merge');
});
it('renders a dropdown with the column options', () => {
expect(findDropdown().exists()).toBe(true);
});
it('renders a dropdown item for each item in columnOptions', () => {
expect(findDropdownItems().length).toBe(Object.keys(defaultProps.columnOptions).length);
});
it('renders a row for every MR', () => {
expect(findMergeRequestTableRows().length).toBe(2);
});
});
describe('computed', () => {
describe('metricDropdownLabel', () => {
it('returns "Time from first comment to last commit"', () => {
expect(wrapper.vm.metricDropdownLabel).toBe('Time from first comment to last commit');
});
});
});
describe('columnMetricChange', () => {
it('it emits the metric key when item is selected from the dropdown', () => {
findFirstDropdownItem().vm.$emit('click');
expect(wrapper.emitted().columnMetricChange[0]).toEqual(['time_to_first_comment']);
});
});
});
const mockMergeRequests = [
{
id: 34,
iid: 10,
description: 'some descirption goes here',
title: 'This is a test MR',
days_to_merge: 24,
time_to_first_comment: 596,
time_to_last_commit: 0,
time_to_merge: 0,
commits_count: 1,
loc_per_commit: 3,
files_touched: 1,
author_avatar_url: null,
merge_request_url: 'http://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/10',
},
{
id: 6,
iid: 6,
description:
'Error temporibus odit veniam expedita ipsa eum quia et. Quo deserunt accusamus ut est ab. Quia sit delectus possimus aut odio veritatis voluptatibus ullam.',
title: 'Vero sint consectetur velit sit totam ipsa aut omnis non repellendus.',
days_to_merge: 139,
time_to_first_comment: 0,
time_to_last_commit: -4530,
time_to_merge: 7866,
commits_count: 3,
loc_per_commit: 4,
files_touched: 3,
author_avatar_url: null,
merge_request_url: 'http://lgitlab.example.com/gitlab-org/gitlab-test/merge_requests/6',
},
];
export default mockMergeRequests;
...@@ -179,6 +179,9 @@ msgstr "" ...@@ -179,6 +179,9 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}" msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "" msgstr ""
msgid "%{count} LOC/commit"
msgstr ""
msgid "%{count} approval required from %{name}" msgid "%{count} approval required from %{name}"
msgid_plural "%{count} approvals required from %{name}" msgid_plural "%{count} approvals required from %{name}"
msgstr[0] "" msgstr[0] ""
...@@ -187,6 +190,9 @@ msgstr[1] "" ...@@ -187,6 +190,9 @@ msgstr[1] ""
msgid "%{count} approvals from %{name}" msgid "%{count} approvals from %{name}"
msgstr "" msgstr ""
msgid "%{count} files touched"
msgstr ""
msgid "%{count} more" msgid "%{count} more"
msgstr "" msgstr ""
...@@ -14523,6 +14529,9 @@ msgstr "" ...@@ -14523,6 +14529,9 @@ msgstr ""
msgid "Time spent" msgid "Time spent"
msgstr "" msgstr ""
msgid "Time to merge"
msgstr ""
msgid "Time tracking" msgid "Time tracking"
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