Commit ed7b6393 authored by Filipa Lacerda's avatar Filipa Lacerda

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

Productivity Analytics: Add MR table to app

See merge request gitlab-org/gitlab-ee!15022
parents 034ab36c e0c88d8c
<script>
export default {};
import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlEmptyState,
GlLoadingIcon,
GlDropdown,
GlDropdownItem,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import MergeRequestTable from './mr_table.vue';
import { chartKeys } from '../constants';
export default {
components: {
GlEmptyState,
GlLoadingIcon,
GlDropdown,
GlDropdownItem,
GlButton,
Icon,
MergeRequestTable,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
endpoint: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
},
data() {
return {
chartKeys,
};
},
computed: {
...mapState('filters', ['groupNamespace', 'projectPath']),
...mapState('table', [
'isLoadingTable',
'mergeRequests',
'pageInfo',
'sortFields',
'columnMetric',
]),
...mapGetters('table', [
'sortFieldDropdownLabel',
'sortIcon',
'sortTooltipTitle',
'getColumnOptions',
'columnMetricLabel',
]),
},
mounted() {
this.setEndpoint(this.endpoint);
},
methods: {
...mapActions(['setEndpoint']),
...mapActions('filters', ['setProjectPath']),
...mapActions('table', [
'setSortField',
'setMergeRequestsPage',
'toggleSortOrder',
'setColumnMetric',
]),
},
};
</script>
<template>
<div>{{ __('Productivity Analytics app goes here') }}</div>
<div>
<gl-empty-state
v-if="!groupNamespace"
class="js-empty-state"
:title="
__('Productivity analytics can help identify the problems that are delaying your team')
"
:svg-path="emptyStateSvgPath"
:description="
__(
'Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones, authors and assignees.',
)
"
/>
<template v-else>
<div
class="qa-mr-table-sort d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2"
>
<h5>{{ __('List') }}</h5>
<div v-if="mergeRequests" class="d-flex flex-column flex-md-row align-items-md-center">
<strong class="mr-2">{{ __('Sort by') }}</strong>
<div class="d-flex">
<gl-dropdown
class="mr-2 flex-grow"
toggle-class="dropdown-menu-toggle"
:text="sortFieldDropdownLabel"
>
<gl-dropdown-item
v-for="(value, key) in sortFields"
:key="key"
active-class="is-active"
class="w-100"
@click="setSortField(key)"
>
{{ value }}
</gl-dropdown-item>
</gl-dropdown>
<gl-button v-gl-tooltip.hover :title="sortTooltipTitle" @click="toggleSortOrder">
<icon :name="sortIcon" />
</gl-button>
</div>
</div>
</div>
<div class="qa-mr-table">
<gl-loading-icon v-if="isLoadingTable" size="md" class="my-4 py-4" />
<merge-request-table
v-else
:merge-requests="mergeRequests"
:page-info="pageInfo"
:column-options="getColumnOptions"
:metric-type="columnMetric"
:metric-label="columnMetricLabel"
@columnMetricChange="setColumnMetric"
@pageChange="setMergeRequestsPage"
/>
</div>
</template>
</div>
</template>
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import MergeRequestTableRow from './mr_table_row.vue';
import Pagination from '~/vue_shared/components/pagination_links.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
MergeRequestTableRow,
Pagination,
},
props: {
mergeRequests: {
type: Array,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
columnOptions: {
type: Object,
required: true,
......@@ -30,6 +36,14 @@ export default {
metricDropdownLabel() {
return this.columnOptions[this.metricType];
},
showPagination() {
return this.pageInfo && this.pageInfo.total;
},
},
methods: {
onPageChange(page) {
this.$emit('pageChange', page);
},
},
};
</script>
......@@ -76,5 +90,12 @@ export default {
/>
</div>
</div>
<pagination
v-if="showPagination"
:change="onPageChange"
:page-info="pageInfo"
class="justify-content-center prepend-top-default"
/>
</div>
</template>
import { __, s__ } from '~/locale';
export const chartKeys = {
main: 'main',
timeBasedHistogram: 'timeBasedHistogram',
commitBasedHistogram: 'commitBasedHistogram',
scatterplot: 'scatterplot',
};
export const metricTypes = [
{
key: 'time_to_first_comment',
label: __('Time from first commit until first comment'),
chart: chartKeys.timeBasedHistogram,
},
{
key: 'time_to_last_commit',
label: __('Time from first comment to last commit'),
chart: chartKeys.timeBasedHistogram,
},
{
key: 'time_to_merge',
label: __('Time from last commit to merge'),
chart: chartKeys.timeBasedHistogram,
},
{
key: 'commits_count',
label: __('Number of commits per MR'),
chart: chartKeys.commitBasedHistogram,
},
{
key: 'loc_per_commit',
label: __('Number of LOCs per commit'),
chart: chartKeys.commitBasedHistogram,
},
{
key: 'files_touched',
label: __('Number of files touched'),
chart: chartKeys.commitBasedHistogram,
},
];
export const tableSortOrder = {
asc: {
title: s__('ProductivityAnalytics|Ascending'),
value: 'asc',
icon: 'sort-lowest',
},
desc: {
title: s__('ProductivityAnalytics|Descending'),
value: 'desc',
icon: 'sort-highest',
},
};
export const timeToMergeMetric = 'time_to_merge';
......@@ -16,6 +16,8 @@ export default () => {
const timeframeContainer = container.querySelector('.js-timeframe-container');
const appContainer = container.querySelector('.js-productivity-analytics-app-container');
const { endpoint, emptyStateSvgPath } = appContainer.dataset;
let filterManager;
// eslint-disable-next-line no-new
......@@ -94,7 +96,12 @@ export default () => {
el: appContainer,
store,
render(h) {
return h(ProductivityAnalyticsApp, {});
return h(ProductivityAnalyticsApp, {
props: {
endpoint,
emptyStateSvgPath,
},
});
},
});
};
import SET_ENDPOINT from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(SET_ENDPOINT, endpoint);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
import filters from './modules/filters/index';
import table from './modules/table/index';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
state: state(),
actions,
mutations,
modules: {
filters,
table,
},
});
......
import * as types from './mutation_types';
export const setGroupNamespace = ({ commit }, groupNamespace) => {
export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
commit(types.SET_GROUP_NAMESPACE, groupNamespace);
dispatch('table/fetchMergeRequests', null, { root: true });
};
export const setProjectPath = ({ commit }, projectPath) => {
export const setProjectPath = ({ commit, dispatch }, projectPath) => {
commit(types.SET_PROJECT_PATH, projectPath);
dispatch('table/fetchMergeRequests', null, { root: true });
};
export const setPath = ({ commit }, path) => {
export const setPath = ({ commit, dispatch }, path) => {
commit(types.SET_PATH, path);
dispatch('table/fetchMergeRequests', null, { root: true });
};
export const setDaysInPast = ({ commit }, days) => {
export const setDaysInPast = ({ commit, dispatch }, days) => {
commit(types.SET_DAYS_IN_PAST, days);
dispatch('table/fetchMergeRequests', null, { root: true });
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
......
import { urlParamsToObject } from '~/lib/utils/common_utils';
/**
* Returns an object of common filter parameters based on the filter's state
* which will be used for querying the API to retrieve chart and MR table data.
* The returned object hast the following form:
*
* {
* group_id: 'gitlab-org',
* project_id: 'gitlab-test',
* author_username: 'author',
* milestone_title: 'my milestone',
* label_name: ['my label', 'yet another label'],
* merged_at_after: '2019-05-09T16:20:18.393Z'
* }
*
*/
export const getCommonFilterParams = (state, getters) => {
const { groupNamespace, projectPath, filters } = state;
const { author_username, milestone_title, label_name } = urlParamsToObject(filters);
return {
group_id: groupNamespace,
project_id: projectPath,
author_username,
milestone_title,
label_name,
merged_at_after: getters.mergedOnAfterDate,
};
};
/**
* Computes the "merged_at_after" date which will be used in the getCommonFilterParams getter.
* It subtracts the number of days (based on the state's daysInPast property) from today's date
* and returns the new date.
*/
export const mergedOnAfterDate = state => {
const d = new Date();
return new Date(d.setTime(d.getTime() - state.daysInPast * 24 * 60 * 60 * 1000)).toISOString();
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state: state(),
mutations,
getters,
actions,
};
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { timeToMergeMetric } from '../../../constants';
export const fetchMergeRequests = ({ dispatch, state, rootState, rootGetters }) => {
dispatch('requestMergeRequests');
const { sortField, sortOrder, pageInfo } = state;
const params = {
...rootGetters['filters/getCommonFilterParams'],
// days_to_merge: rootState.charts.charts.main.selected,
sort: `${sortField}_${sortOrder}`,
page: pageInfo ? pageInfo.page : null,
};
return axios
.get(rootState.endpoint, { params })
.then(response => {
const { headers, data } = response;
dispatch('receiveMergeRequestsSuccess', { headers, data });
})
.catch(() => {
dispatch('receiveMergeRequestsError');
});
};
export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsSuccess = ({ commit }, { headers, data: mergeRequests }) => {
const normalizedHeaders = normalizeHeaders(headers);
const pageInfo = parseIntPagination(normalizedHeaders);
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { pageInfo, mergeRequests });
};
export const receiveMergeRequestsError = ({ commit }) => commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
export const setSortField = ({ commit, dispatch }, data) => {
commit(types.SET_SORT_FIELD, data);
// let's make sure we update the column that we sort on (except for 'time_to_merge')
if (data !== timeToMergeMetric) {
dispatch('setColumnMetric', data);
}
dispatch('fetchMergeRequests');
};
export const toggleSortOrder = ({ commit, dispatch }) => {
commit(types.TOGGLE_SORT_ORDER);
dispatch('fetchMergeRequests');
};
export const setColumnMetric = ({ commit }, data) => commit(types.SET_COLUMN_METRIC, data);
export const setMergeRequestsPage = ({ commit, dispatch }, data) => {
commit(types.SET_MERGE_REQUESTS_PAGE, data);
dispatch('fetchMergeRequests');
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { tableSortOrder } from './../../../constants';
export const sortIcon = state => tableSortOrder[state.sortOrder].icon;
export const sortTooltipTitle = state => tableSortOrder[state.sortOrder].title;
export const sortFieldDropdownLabel = state => state.sortFields[state.sortField];
export const getColumnOptions = state =>
Object.keys(state.sortFields)
.filter(key => key !== 'time_to_merge')
.reduce((obj, key) => {
const result = { ...obj, [key]: state.sortFields[key] };
return result;
}, {});
export const columnMetricLabel = (state, getters) => getters.getColumnOptions[state.columnMetric];
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state: state(),
mutations,
getters,
actions,
};
export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS';
export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS';
export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR';
export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const TOGGLE_SORT_ORDER = 'TOGGLE_SORT_ORDER';
export const SET_COLUMN_METRIC = 'SET_COLUMN_METRIC';
export const SET_MERGE_REQUESTS_PAGE = 'SET_MERGE_REQUESTS_PAGE';
import * as types from './mutation_types';
import { tableSortOrder } from './../../../constants';
export default {
[types.REQUEST_MERGE_REQUESTS](state) {
state.isLoadingTable = true;
},
[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { pageInfo, mergeRequests }) {
state.isLoadingTable = false;
state.hasError = false;
state.pageInfo = pageInfo;
state.mergeRequests = mergeRequests;
},
[types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
state.isLoadingTable = false;
state.hasError = true;
state.pageInfo = {};
state.mergeRequests = [];
},
[types.SET_SORT_FIELD](state, sortField) {
state.sortField = sortField;
},
[types.TOGGLE_SORT_ORDER](state) {
state.sortOrder =
state.sortOrder === tableSortOrder.asc.value
? tableSortOrder.desc.value
: tableSortOrder.asc.value;
},
[types.SET_COLUMN_METRIC](state, columnMetric) {
state.columnMetric = columnMetric;
},
[types.SET_MERGE_REQUESTS_PAGE](state, page) {
state.pageInfo = { ...state.pageInfo, page };
},
};
import { __ } from '~/locale';
import { chartKeys, tableSortOrder, metricTypes } from './../../../constants';
const sortFields = metricTypes.reduce(
(acc, curr) => {
const { key, label, chart } = curr;
if (chart === chartKeys.timeBasedHistogram) {
acc[key] = label;
}
return acc;
},
{ days_to_merge: __('Days to merge') },
);
export default () => ({
isLoadingTable: false,
hasError: false,
mergeRequests: [],
pageInfo: {},
sortOrder: tableSortOrder.asc.value,
sortFields,
sortField: 'time_to_merge',
columnMetric: 'time_to_first_comment',
});
import SET_ENDPOINT from './mutation_types';
export default {
[SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
};
......@@ -8,4 +8,4 @@
= render 'shared/issuable/search_bar', type: :productivity_analytics
.dropdown-container
.js-timeframe-container
.js-productivity-analytics-app-container
.js-productivity-analytics-app-container{ data: { endpoint: analytics_productivity_analytics_path, empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg') } }
......@@ -87,5 +87,7 @@ exports[`MergeRequestTable component matches the snapshot 1`] = `
/>
</div>
</div>
<!---->
</div>
`;
......@@ -16,6 +16,7 @@ describe('MergeRequestTable component', () => {
},
metricType: 'time_to_last_commit',
metricLabel: 'Time from first comment to last commit',
pageInfo: {},
};
const factory = (props = defaultProps) => {
......
......@@ -16,7 +16,12 @@ describe('Productivity analytics filter actions', () => {
payload: 'gitlab-org',
},
],
[],
[
{
type: 'table/fetchMergeRequests',
payload: null,
},
],
done,
));
});
......@@ -33,7 +38,12 @@ describe('Productivity analytics filter actions', () => {
payload: 'gitlab-test',
},
],
[],
[
{
type: 'table/fetchMergeRequests',
payload: null,
},
],
done,
));
});
......@@ -50,7 +60,12 @@ describe('Productivity analytics filter actions', () => {
payload: 'author_username=root',
},
],
[],
[
{
type: 'table/fetchMergeRequests',
payload: null,
},
],
done,
));
});
......@@ -67,7 +82,12 @@ describe('Productivity analytics filter actions', () => {
payload: 90,
},
],
[],
[
{
type: 'table/fetchMergeRequests',
payload: null,
},
],
done,
));
});
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/analytics/productivity_analytics/store/modules/table/actions';
import * as types from 'ee/analytics/productivity_analytics/store/modules/table/mutation_types';
import getInitialState from 'ee/analytics/productivity_analytics/store/modules/table/state';
import mockMergeRequests from '../../../mock_data';
describe('Productivity analytics table actions', () => {
let mockedContext;
let mockedState;
let mock;
const groupNamespace = 'gitlab-org';
const projectPath = 'gitlab-test';
const filterParams = {
days_to_merge: [5],
sort: 'time_to_merge_asc',
};
const pageInfo = {
page: 1,
nextPage: 2,
previousPage: 1,
perPage: 10,
total: 50,
totalPages: 5,
};
const headers = {
'X-Next-Page': pageInfo.nextPage,
'X-Page': pageInfo.page,
'X-Per-Page': pageInfo.perPage,
'X-Prev-Page': pageInfo.previousPage,
'X-Total': pageInfo.total,
'X-Total-Pages': pageInfo.totalPages,
};
beforeEach(() => {
mockedContext = {
dispatch() {},
rootState: {
charts: {
charts: {
main: {
selected: [5],
},
},
},
endpoint: `${TEST_HOST}/analytics/productivity_analytics.json`,
},
getters: {
getFilterParams: () => filterParams,
},
rootGetters: {
// eslint-disable-next-line no-useless-computed-key
['filters/getCommonFilterParams']: {
group_id: groupNamespace,
project_id: projectPath,
},
},
state: getInitialState(),
};
// testAction looks for rootGetters in state,
// so they need to be concatenated here.
mockedState = {
...mockedContext.state,
...mockedContext.getters,
...mockedContext.rootGetters,
...mockedContext.rootState,
};
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('fetchMergeRequests', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(mockedState.endpoint).replyOnce(200, mockMergeRequests, headers);
});
// This gets uncommented with the API changes from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/14772
/*
it('calls API with pparams', () => {
jest.spyOn(axios, 'get');
actions.fetchMergeRequests(mockedContext);
expect(axios.get).toHaveBeenCalledWith(mockedState.endpoint, {
params: {
group_id: groupNamespace,
project_id: projectPath,
days_to_merge: [5],
sort: 'time_to_merge_asc',
},
});
});
*/
it('dispatches success with received data', done =>
testAction(
actions.fetchMergeRequests,
null,
mockedState,
[],
[
{ type: 'requestMergeRequests' },
{
type: 'receiveMergeRequestsSuccess',
payload: { data: mockMergeRequests, headers },
},
],
done,
));
});
describe('error', () => {
beforeEach(() => {
mock.onGet(mockedState.endpoint).replyOnce(500);
});
it('dispatches error', done =>
testAction(
actions.fetchMergeRequests,
null,
mockedState,
[],
[{ type: 'requestMergeRequests' }, { type: 'receiveMergeRequestsError' }],
done,
));
});
});
describe('requestMergeRequests', () => {
it('should commit the request mutation', done =>
testAction(
actions.requestMergeRequests,
null,
mockedContext.state,
[{ type: types.REQUEST_MERGE_REQUESTS }],
[],
done,
));
});
describe('receiveMergeRequestsSuccess', () => {
it('should commit received data', done =>
testAction(
actions.receiveMergeRequestsSuccess,
{ headers, data: mockMergeRequests },
mockedContext.state,
[
{
type: types.RECEIVE_MERGE_REQUESTS_SUCCESS,
payload: { pageInfo, mergeRequests: mockMergeRequests },
},
],
[],
done,
));
});
describe('receiveMergeRequestsError', () => {
it('should commit error', done =>
testAction(
actions.receiveMergeRequestsError,
null,
mockedContext.state,
[{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }],
[],
done,
));
});
describe('setSortField', () => {
it('should commit setSortField', done =>
testAction(
actions.setSortField,
'time_to_last_commit',
mockedContext.state,
[{ type: types.SET_SORT_FIELD, payload: 'time_to_last_commit' }],
[
{ type: 'setColumnMetric', payload: 'time_to_last_commit' },
{ type: 'fetchMergeRequests' },
],
done,
));
it('should not dispatch setColumnMetric when metric is "time_to_merge"', done =>
testAction(
actions.setSortField,
'time_to_merge',
mockedContext.state,
[{ type: types.SET_SORT_FIELD, payload: 'time_to_merge' }],
[{ type: 'fetchMergeRequests' }],
done,
));
});
describe('toggleSortOrder', () => {
it('should commit toggleSortOrder', done =>
testAction(
actions.toggleSortOrder,
null,
mockedContext.state,
[{ type: types.TOGGLE_SORT_ORDER }],
[{ type: 'fetchMergeRequests' }],
done,
));
});
describe('setColumnMetric', () => {
it('should commit setColumnMetric', done =>
testAction(
actions.setColumnMetric,
'time_to_first_comment',
mockedContext.state,
[{ type: types.SET_COLUMN_METRIC, payload: 'time_to_first_comment' }],
[],
done,
));
});
describe('setMergeRequestsPage', () => {
it('should commit setMergeRequestsPage', done =>
testAction(
actions.setMergeRequestsPage,
2,
mockedContext.state,
[{ type: types.SET_MERGE_REQUESTS_PAGE, payload: 2 }],
[{ type: 'fetchMergeRequests' }],
done,
));
});
});
import createState from 'ee/analytics/productivity_analytics/store/modules/table/state';
import * as getters from 'ee/analytics/productivity_analytics/store/modules/table/getters';
import { tableSortOrder } from 'ee/analytics/productivity_analytics/constants';
describe('Productivity analytics table getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('sortIcon', () => {
it('returns the correct icon when sort order is asc', () => {
state = {
sortOrder: tableSortOrder.asc.value,
};
expect(getters.sortIcon(state)).toBe('sort-lowest');
});
it('returns the correct icon when sort order is desc', () => {
state = {
sortOrder: tableSortOrder.desc.value,
};
expect(getters.sortIcon(state)).toBe('sort-highest');
});
});
describe('sortTooltipTitle', () => {
it('returns the correct title when sort order is asc', () => {
state = {
sortOrder: tableSortOrder.asc.value,
};
expect(getters.sortTooltipTitle(state)).toBe('Ascending');
});
it('returns the correct title when sort order is desc', () => {
state = {
sortOrder: tableSortOrder.desc.value,
};
expect(getters.sortTooltipTitle(state)).toBe('Descending');
});
});
describe('sortFieldDropdownLabel', () => {
it('returns the correct label for the current sort field', () => {
state.sortField = 'time_to_last_commit';
expect(getters.sortFieldDropdownLabel(state)).toBe('Time from first comment to last commit');
});
});
describe('getColumnOptions', () => {
it('returns an object of key/value pairs with the available column options', () => {
state.sortFields = {
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',
days_to_merge: 'Days to merge',
};
expect(getters.getColumnOptions(state)).toEqual({
days_to_merge: 'Days to merge',
time_to_first_comment: 'Time from first commit until first comment',
time_to_last_commit: 'Time from first comment to last commit',
});
});
});
});
import * as types from 'ee/analytics/productivity_analytics/store/modules/table/mutation_types';
import mutations from 'ee/analytics/productivity_analytics/store/modules/table/mutations';
import getInitialState from 'ee/analytics/productivity_analytics/store/modules/table/state';
import { tableSortOrder } from 'ee/analytics/productivity_analytics/constants';
import mockMergeRequests from '../../../mock_data';
describe('Productivity analytics table mutations', () => {
let state;
const pageInfo = {
a: 1,
b: 2,
c: 3,
};
beforeEach(() => {
state = getInitialState();
});
describe(types.REQUEST_MERGE_REQUESTS, () => {
it('sets isLoadingTable to true', () => {
mutations[types.REQUEST_MERGE_REQUESTS](state);
expect(state.isLoadingTable).toBe(true);
});
});
describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
it('updates table with data', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, {
pageInfo,
mergeRequests: mockMergeRequests,
});
expect(state.isLoadingTable).toBe(false);
expect(state.hasError).toBe(false);
expect(state.mergeRequests).toEqual(mockMergeRequests);
expect(state.pageInfo).toEqual(pageInfo);
});
});
describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
it('sets isError and clears data', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](state);
expect(state.isLoadingTable).toBe(false);
expect(state.hasError).toBe(true);
expect(state.mergeRequests).toEqual([]);
expect(state.pageInfo).toEqual({});
});
});
describe(types.SET_SORT_FIELD, () => {
it('sets sortField to "time_to_last_commit"', () => {
const sortField = 'time_to_last_commit';
mutations[types.SET_SORT_FIELD](state, sortField);
expect(state.sortField).toBe(sortField);
});
});
describe(types.TOGGLE_SORT_ORDER, () => {
it('sets sortOrder "asc" when currently "desc"', () => {
state.sortOrder = tableSortOrder.desc.value;
mutations[types.TOGGLE_SORT_ORDER](state);
expect(state.sortOrder).toBe(tableSortOrder.asc.value);
});
it('sets sortOrder "desc" when currently "asc"', () => {
state.sortOrder = tableSortOrder.asc.value;
mutations[types.TOGGLE_SORT_ORDER](state);
expect(state.sortOrder).toBe(tableSortOrder.desc.value);
});
});
describe(types.SET_COLUMN_METRIC, () => {
it('sets columnMetric to "time_to_first_comment"', () => {
const columnMetric = 'time_to_first_comment';
mutations[types.SET_COLUMN_METRIC](state, columnMetric);
expect(state.columnMetric).toBe(columnMetric);
});
});
});
......@@ -4460,6 +4460,9 @@ msgstr ""
msgid "DayTitle|W"
msgstr ""
msgid "Days to merge"
msgstr ""
msgid "Debug"
msgstr ""
......@@ -9905,6 +9908,15 @@ msgstr ""
msgid "Number of Elasticsearch shards"
msgstr ""
msgid "Number of LOCs per commit"
msgstr ""
msgid "Number of commits per MR"
msgstr ""
msgid "Number of files touched"
msgstr ""
msgid "OK"
msgstr ""
......@@ -10780,7 +10792,13 @@ msgstr ""
msgid "Productivity Analytics"
msgstr ""
msgid "Productivity Analytics app goes here"
msgid "Productivity analytics can help identify the problems that are delaying your team"
msgstr ""
msgid "ProductivityAnalytics|Ascending"
msgstr ""
msgid "ProductivityAnalytics|Descending"
msgstr ""
msgid "Profile"
......@@ -13784,6 +13802,9 @@ msgstr ""
msgid "Start by choosing a group to see how your team is spending time. You can then drill down to the project level."
msgstr ""
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones, authors and assignees."
msgstr ""
msgid "Start cleanup"
msgstr ""
......@@ -15037,6 +15058,15 @@ msgstr ""
msgid "Time estimate"
msgstr ""
msgid "Time from first comment to last commit"
msgstr ""
msgid "Time from first commit until first comment"
msgstr ""
msgid "Time from last commit to merge"
msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
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