Commit c7530356 authored by Martin Wortschack's avatar Martin Wortschack Committed by Fatih Acet

Add MR table components

- Add MR table and MR table row component
- Add table styles
- Update PO file
- Add specs
- Add snapshots
- Add mw-90 utility class
parent 40fb30f2
<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 ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
msgid "%{count} LOC/commit"
msgstr ""
msgid "%{count} approval required from %{name}"
msgid_plural "%{count} approvals required from %{name}"
msgstr[0] ""
......@@ -187,6 +190,9 @@ msgstr[1] ""
msgid "%{count} approvals from %{name}"
msgstr ""
msgid "%{count} files touched"
msgstr ""
msgid "%{count} more"
msgstr ""
......@@ -14523,6 +14529,9 @@ msgstr ""
msgid "Time spent"
msgstr ""
msgid "Time to merge"
msgstr ""
msgid "Time tracking"
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