Commit 4a0a2b10 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Added Top N labels request api spec

Minor cleanup linting

Added action specs for fetchTopRankedGroupLabels

Added mutation specs for top ranked labels

Updated tests with cases for testing
the top ranked labels request
parent 08cc02f8
...@@ -475,7 +475,6 @@ img.emoji { ...@@ -475,7 +475,6 @@ img.emoji {
.mw-90p { max-width: 90%; } .mw-90p { max-width: 90%; }
.min-height-0 { min-height: 0; } .min-height-0 { min-height: 0; }
.min-width-300 { min-height: 300px; }
.svg-w-100 { .svg-w-100 {
svg { svg {
......
...@@ -113,7 +113,7 @@ export default { ...@@ -113,7 +113,7 @@ export default {
startDate, startDate,
endDate, endDate,
selectedProjectIds, selectedProjectIds,
tasksByType: { subject, labelIds: selectedLabelIds }, tasksByType: { subject, selectedLabelIds },
} = this; } = this;
return { return {
selectedGroup, selectedGroup,
......
...@@ -132,8 +132,8 @@ export default { ...@@ -132,8 +132,8 @@ export default {
<icon :size="16" name="settings" /> <icon :size="16" name="settings" />
<icon :size="16" name="chevron-down" /> <icon :size="16" name="chevron-down" />
</gl-button> </gl-button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right min-width-300"> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-right">
<div class="js-tasks-by-type-chart-filters-subject mb-3 mx-3"> <div class="js-tasks-by-type-chart-filters-subject mb-3 px-3">
<p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p> <p class="font-weight-bold text-left mb-2">{{ s__('CycleAnalytics|Show') }}</p>
<gl-segmented-control <gl-segmented-control
v-model="selectedSubjectFilter" v-model="selectedSubjectFilter"
...@@ -145,7 +145,7 @@ export default { ...@@ -145,7 +145,7 @@ export default {
/> />
</div> </div>
<gl-dropdown-divider /> <gl-dropdown-divider />
<div class="js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100"> <div class="js-tasks-by-type-chart-filters-labels mb-3 px-3">
<p class="font-weight-bold text-left my-2"> <p class="font-weight-bold text-left my-2">
{{ s__('CycleAnalytics|Select labels') }} {{ s__('CycleAnalytics|Select labels') }}
<br /> <br />
...@@ -155,7 +155,7 @@ export default { ...@@ -155,7 +155,7 @@ export default {
<input class="dropdown-input-field" type="search" /> <input class="dropdown-input-field" type="search" />
<icon name="search" class="dropdown-input-search" data-hidden="true" /> <icon name="search" class="dropdown-input-search" data-hidden="true" />
</div> </div>
<div class="dropdown-content px-0 w-100"></div> <div class="dropdown-content px-0"></div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -270,7 +270,7 @@ export const receiveTopRankedGroupLabelsSuccess = ({ commit }, data) => ...@@ -270,7 +270,7 @@ export const receiveTopRankedGroupLabelsSuccess = ({ commit }, data) =>
export const receiveTopRankedGroupLabelsError = ({ commit }, error) => { export const receiveTopRankedGroupLabelsError = ({ commit }, error) => {
commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR, error); commit(types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR, error);
createFlash(__('There was an error fetching label data for the selected group')); createFlash(__('There was an error fetching the top labels for the selected group'));
}; };
export const requestTopRankedGroupLabels = ({ commit }) => export const requestTopRankedGroupLabels = ({ commit }) =>
...@@ -409,22 +409,22 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => { ...@@ -409,22 +409,22 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
const { const {
currentGroupPath, currentGroupPath,
cycleAnalyticsRequestParams: { created_after, created_before, project_ids }, cycleAnalyticsRequestParams: { created_after, created_before, project_ids },
topRankedLabelIds,
} = getters; } = getters;
const { const {
tasksByType: { labelIds, subject }, tasksByType: { subject },
} = state; } = state;
// dont request if we have no labels selected...for now // dont request if we have no labels selected...for now
if (labelIds.length) { if (topRankedLabelIds.length) {
const params = { const params = {
group_id: currentGroupPath, group_id: currentGroupPath,
created_after, created_after,
created_before, created_before,
project_ids, project_ids,
subject, subject,
label_ids: labelIds, label_ids: topRankedLabelIds,
}; };
dispatch('requestTasksByTypeData'); dispatch('requestTasksByTypeData');
......
...@@ -11,6 +11,9 @@ export const currentGroupPath = ({ selectedGroup }) => ...@@ -11,6 +11,9 @@ export const currentGroupPath = ({ selectedGroup }) =>
export const selectedProjectIds = ({ selectedProjects }) => export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects.length ? selectedProjects.map(({ id }) => id) : []; selectedProjects.length ? selectedProjects.map(({ id }) => id) : [];
export const topRankedLabelIds = ({ topRankedLabels }) =>
topRankedLabels.length ? topRankedLabels.map(({ id }) => id) : [];
export const cycleAnalyticsRequestParams = ({ startDate = null, endDate = null }, getters) => ({ export const cycleAnalyticsRequestParams = ({ startDate = null, endDate = null }, getters) => ({
project_ids: getters.selectedProjectIds, project_ids: getters.selectedProjectIds,
created_after: startDate ? dateFormat(startDate, dateFormats.isoDate) : null, created_after: startDate ? dateFormat(startDate, dateFormats.isoDate) : null,
......
...@@ -69,16 +69,15 @@ export default { ...@@ -69,16 +69,15 @@ export default {
state.topRankedLabels = []; state.topRankedLabels = [];
state.tasksByType = { state.tasksByType = {
...state.tasksByType, ...state.tasksByType,
labelIds: [], selectedLabelIds: [],
}; };
}, },
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS](state, data = []) { [types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS](state, data = []) {
const { tasksByType } = state; const { tasksByType } = state;
state.topRankedLabels = data.length ? data.map(convertObjectPropsToCamelCase) : []; state.topRankedLabels = data.length ? data.map(convertObjectPropsToCamelCase) : [];
// TODO: Should probably append as many labels from `labels` as we need if we dont get enough returned
state.tasksByType = { state.tasksByType = {
...tasksByType, ...tasksByType,
labelIds: data.length ? data.map(({ id }) => id) : [], selectedLabelIds: data.length ? data.map(({ id }) => id) : [],
}; };
}, },
[types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR](state) { [types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR](state) {
...@@ -86,7 +85,7 @@ export default { ...@@ -86,7 +85,7 @@ export default {
state.topRankedLabels = []; state.topRankedLabels = [];
state.tasksByType = { state.tasksByType = {
...tasksByType, ...tasksByType,
labelIds: [], selectedLabelIds: [],
}; };
}, },
[types.REQUEST_STAGE_MEDIANS](state) { [types.REQUEST_STAGE_MEDIANS](state) {
......
...@@ -37,7 +37,7 @@ export default () => ({ ...@@ -37,7 +37,7 @@ export default () => ({
tasksByType: { tasksByType: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE, subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
labelIds: [], selectedLabelIds: [],
data: [], data: [],
}, },
......
...@@ -12,19 +12,19 @@ exports[`TasksByTypeChart no data available should render the no data available ...@@ -12,19 +12,19 @@ exports[`TasksByTypeChart no data available should render the no data available
`; `;
exports[`TasksByTypeChart with data available filters labels has label filters 1`] = ` exports[`TasksByTypeChart with data available filters labels has label filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 mx-3 w-100\\"> "<div class=\\"js-tasks-by-type-chart-filters-labels mb-3 px-3\\">
<p class=\\"font-weight-bold text-left my-2\\"> <p class=\\"font-weight-bold text-left my-2\\">
Select labels Select labels
<br> <small>3 selected (15 max)</small></p> <br> <small>3 selected (15 max)</small></p>
<div class=\\"dropdown-input px-0\\"><input type=\\"search\\" class=\\"dropdown-input-field\\"> <svg aria-hidden=\\"true\\" class=\\"dropdown-input-search s16 ic-search\\" data-hidden=\\"true\\"> <div class=\\"dropdown-input px-0\\"><input type=\\"search\\" class=\\"dropdown-input-field\\"> <svg aria-hidden=\\"true\\" class=\\"dropdown-input-search s16 ic-search\\" data-hidden=\\"true\\">
<use xlink:href=\\"#search\\"></use> <use xlink:href=\\"#search\\"></use>
</svg></div> </svg></div>
<div class=\\"dropdown-content px-0 w-100\\"></div> <div class=\\"dropdown-content px-0\\"></div>
</div>" </div>"
`; `;
exports[`TasksByTypeChart with data available filters labels with label dropdown open renders the group labels as dropdown items 1`] = ` exports[`TasksByTypeChart with data available filters labels with label dropdown open renders the group labels as dropdown items 1`] = `
"<div class=\\"dropdown-content px-0 w-100\\"> "<div class=\\"dropdown-content px-0\\">
<ul> <ul>
<li> <li>
<a href=\\"#\\" class=\\"dropdown-menu-link \\"> <a href=\\"#\\" class=\\"dropdown-menu-link \\">
...@@ -54,7 +54,7 @@ exports[`TasksByTypeChart with data available filters labels with label dropdown ...@@ -54,7 +54,7 @@ exports[`TasksByTypeChart with data available filters labels with label dropdown
`; `;
exports[`TasksByTypeChart with data available filters subject has subject filters 1`] = ` exports[`TasksByTypeChart with data available filters subject has subject filters 1`] = `
"<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 mx-3\\"> "<div class=\\"js-tasks-by-type-chart-filters-subject mb-3 px-3\\">
<p class=\\"font-weight-bold text-left mb-2\\">Show</p> <p class=\\"font-weight-bold text-left mb-2\\">Show</p>
<div role=\\"radiogroup\\" tabindex=\\"-1\\" class=\\"gl-segmented-control btn-group-toggle btn-group\\" id=\\"__BVID__74\\"><label class=\\"btn btn-gl-segmented-button active\\"><input id=\\"__BVID__74__BV_option_0_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"Issue\\"><span>Issues</span></label><label class=\\"btn btn-gl-segmented-button\\"><input id=\\"__BVID__74__BV_option_1_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"MergeRequest\\"><span>Merge Requests</span></label></div> <div role=\\"radiogroup\\" tabindex=\\"-1\\" class=\\"gl-segmented-control btn-group-toggle btn-group\\" id=\\"__BVID__74\\"><label class=\\"btn btn-gl-segmented-button active\\"><input id=\\"__BVID__74__BV_option_0_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"Issue\\"><span>Issues</span></label><label class=\\"btn btn-gl-segmented-button\\"><input id=\\"__BVID__74__BV_option_1_\\" type=\\"radio\\" name=\\"__BVID__74\\" autocomplete=\\"off\\" class=\\"\\" value=\\"MergeRequest\\"><span>Merge Requests</span></label></div>
</div>" </div>"
......
...@@ -6,7 +6,10 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -6,7 +6,10 @@ import testAction from 'helpers/vuex_action_helper';
import * as getters from 'ee/analytics/cycle_analytics/store/getters'; import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import * as actions from 'ee/analytics/cycle_analytics/store/actions'; import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { TASKS_BY_TYPE_FILTERS } from 'ee/analytics/cycle_analytics/constants'; import {
TASKS_BY_TYPE_FILTERS,
TASKS_BY_TYPE_SUBJECT_ISSUE,
} from 'ee/analytics/cycle_analytics/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { toYmd } from 'ee/analytics/shared/utils'; import { toYmd } from 'ee/analytics/shared/utils';
...@@ -332,6 +335,72 @@ describe('Cycle analytics actions', () => { ...@@ -332,6 +335,72 @@ describe('Cycle analytics actions', () => {
}); });
}); });
describe('fetchTopRankedGroupLabels', () => {
beforeEach(() => {
gon.api_version = 'v4';
state = { selectedGroup, tasksByType: { subject: TASKS_BY_TYPE_SUBJECT_ISSUE } };
});
describe('succeeds', () => {
beforeEach(() => {
mock.onGet(endpoints.tasksByTypeTopLabelsData).replyOnce(200, groupLabels);
});
it('dispatches receiveTopRankedGroupLabelsSuccess if the request succeeds', () => {
const dispatch = jest.fn();
return actions
.fetchTopRankedGroupLabels({
dispatch,
state,
getters,
})
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestTopRankedGroupLabels');
expect(dispatch).toHaveBeenCalledWith(
'receiveTopRankedGroupLabelsSuccess',
groupLabels,
);
});
});
});
describe('with an error', () => {
beforeEach(() => {
mock.onGet(endpoints.fetchTopRankedGroupLabels).replyOnce(404);
});
it('dispatches receiveTopRankedGroupLabelsError if the request fails', () => {
const dispatch = jest.fn();
return actions
.fetchTopRankedGroupLabels({
dispatch,
state,
getters,
})
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestTopRankedGroupLabels');
expect(dispatch).toHaveBeenCalledWith('receiveTopRankedGroupLabelsError', error);
});
});
});
describe('receiveTopRankedGroupLabelsError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('flashes an error message if the request fails', () => {
actions.receiveTopRankedGroupLabelsError({
commit: () => {},
});
shouldFlashAMessage('There was an error fetching the top labels for the selected group');
});
});
});
describe('fetchCycleAnalyticsData', () => { describe('fetchCycleAnalyticsData', () => {
function mockFetchCycleAnalyticsAction(overrides = {}) { function mockFetchCycleAnalyticsAction(overrides = {}) {
const mocks = { const mocks = {
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
durationChartPlottableMedianData, durationChartPlottableMedianData,
allowedStages, allowedStages,
selectedProjects, selectedProjects,
groupLabels,
} from '../mock_data'; } from '../mock_data';
let state = null; let state = null;
...@@ -30,6 +31,44 @@ describe('Cycle analytics getters', () => { ...@@ -30,6 +31,44 @@ describe('Cycle analytics getters', () => {
}); });
}); });
describe('selectedProjectIds', () => {
describe('with selectedProjects set', () => {
it('returns the ids of each project', () => {
state = {
selectedProjects,
};
expect(getters.selectedProjectIds(state)).toEqual(selectedProjects.map(({ id }) => id));
});
});
describe('without selectedProjects set', () => {
it('will return an empty array', () => {
state = { selectedProjects: [] };
expect(getters.selectedProjectIds(state)).toEqual([]);
});
});
});
describe('topRankedLabelIds', () => {
describe('with topRankedLabels set', () => {
it('returns the `fullPath` value of the group', () => {
state = {
topRankedLabels: groupLabels,
};
expect(getters.topRankedLabelIds(state)).toEqual(groupLabels.map(({ id }) => id));
});
});
describe('without topRankedLabels set', () => {
it('will return an empty array', () => {
state = { topRankedLabels: [] };
expect(getters.topRankedLabelIds(state)).toEqual([]);
});
});
});
describe('currentGroupPath', () => { describe('currentGroupPath', () => {
describe('with selectedGroup set', () => { describe('with selectedGroup set', () => {
it('returns the `fullPath` value of the group', () => { it('returns the `fullPath` value of the group', () => {
......
...@@ -53,6 +53,8 @@ describe('Cycle analytics mutations', () => { ...@@ -53,6 +53,8 @@ describe('Cycle analytics mutations', () => {
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true} ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]} ${types.REQUEST_GROUP_LABELS} | ${'labels'} | ${[]}
${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]} ${types.RECEIVE_GROUP_LABELS_ERROR} | ${'labels'} | ${[]}
${types.REQUEST_TOP_RANKED_GROUP_LABELS} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_TOP_RANKED_GROUP_LABELS_ERROR} | ${'topRankedLabels'} | ${[]}
${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]} ${types.RECEIVE_SUMMARY_DATA_ERROR} | ${'summary'} | ${[]}
${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]} ${types.REQUEST_SUMMARY_DATA} | ${'summary'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]} ${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]}
...@@ -154,6 +156,14 @@ describe('Cycle analytics mutations', () => { ...@@ -154,6 +156,14 @@ describe('Cycle analytics mutations', () => {
}); });
}); });
describe(`${types.RECEIVE_TOP_RANKED_GROUP_LABELS_SUCCESS}`, () => {
it('will set the labels state item with the camelCased group labels', () => {
mutations[types.RECEIVE_GROUP_LABELS_SUCCESS](state, groupLabels);
expect(state.labels).toEqual(groupLabels.map(convertObjectPropsToCamelCase));
});
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => { describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false and errorCode=null', () => { it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, { mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......
...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import Api from 'ee/api'; import Api from 'ee/api';
import * as cycleAnalyticsConstants from 'ee/analytics/cycle_analytics/constants'; import * as cycleAnalyticsConstants from 'ee/analytics/cycle_analytics/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as analyticsMockData from 'ee_jest/analytics/cycle_analytics/mock_data';
describe('Api', () => { describe('Api', () => {
const dummyApiVersion = 'v3000'; const dummyApiVersion = 'v3000';
...@@ -353,13 +354,38 @@ describe('Api', () => { ...@@ -353,13 +354,38 @@ describe('Api', () => {
subject: cycleAnalyticsConstants.TASKS_BY_TYPE_SUBJECT_ISSUE, subject: cycleAnalyticsConstants.TASKS_BY_TYPE_SUBJECT_ISSUE,
label_ids: labelIds, label_ids: labelIds,
}; };
const expectedUrl = `${dummyUrlRoot}/-/analytics/type_of_work/tasks_by_type`; const expectedUrl = analyticsMockData.endpoints.tasksByTypeData;
mock.onGet(expectedUrl).reply(200, tasksByTypeResponse); mock.onGet(expectedUrl).reply(200, tasksByTypeResponse);
Api.cycleAnalyticsTasksByType({ params }) Api.cycleAnalyticsTasksByType(params)
.then(({ data, config: { params: reqParams } }) => { .then(({ data, config: { params: reqParams } }) => {
expect(data).toEqual(tasksByTypeResponse); expect(data).toEqual(tasksByTypeResponse);
expect(reqParams.params).toEqual(params); expect(reqParams).toEqual(params);
})
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsTopLabels', () => {
it('fetches top group level labels', done => {
const response = [];
const labelIds = [10, 9, 8, 7];
const params = {
...defaultParams,
project_ids: null,
subject: cycleAnalyticsConstants.TASKS_BY_TYPE_SUBJECT_ISSUE,
label_ids: labelIds,
};
const expectedUrl = analyticsMockData.endpoints.tasksByTypeTopLabelsData;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsTopLabels(params)
.then(({ data, config: { url, params: reqParams } }) => {
expect(data).toEqual(response);
expect(url).toMatch(expectedUrl);
expect(reqParams).toEqual(params);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -6110,6 +6110,9 @@ msgstr "" ...@@ -6110,6 +6110,9 @@ msgstr ""
msgid "CycleAnalyticsStage|should be under a group" msgid "CycleAnalyticsStage|should be under a group"
msgstr "" msgstr ""
msgid "CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)"
msgstr ""
msgid "CycleAnalytics|%{stageCount} stages selected" msgid "CycleAnalytics|%{stageCount} stages selected"
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