Commit 3894edef authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Fatih Acet

Move cycle analytics endpoints to api.js

Moves the summary data request and the
group stages and events request to the api.js
module
parent b0182221
......@@ -92,8 +92,6 @@ export default {
'fetchCycleAnalyticsData',
'fetchStageData',
'fetchGroupStagesAndEvents',
'setCycleAnalyticsDataEndpoint',
'setStageDataEndpoint',
'setSelectedGroup',
'setSelectedProjects',
'setSelectedTimeframe',
......@@ -106,7 +104,6 @@ export default {
'fetchTasksByTypeData',
]),
onGroupSelect(group) {
this.setCycleAnalyticsDataEndpoint(group.full_path);
this.setSelectedGroup(group);
this.fetchCycleAnalyticsData();
},
......@@ -118,8 +115,7 @@ export default {
onStageSelect(stage) {
this.hideCustomStageForm();
this.setSelectedStageId(stage.id);
this.setStageDataEndpoint(this.currentStage.slug);
this.fetchStageData(this.currentStage.name);
this.fetchStageData(this.currentStage.slug);
},
onShowAddStageForm() {
this.showCustomStageForm();
......
import axios from '~/lib/utils/axios_utils';
import createFlash, { hideFlash } from '~/flash';
import { __ } from '~/locale';
import Api from 'ee/api';
......@@ -13,11 +12,6 @@ const removeError = () => {
}
};
export const setCycleAnalyticsDataEndpoint = ({ commit }, groupPath) =>
commit(types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT, groupPath);
export const setStageDataEndpoint = ({ commit }, stageSlug) =>
commit(types.SET_STAGE_DATA_ENDPOINT, stageSlug);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
export const setSelectedProjects = ({ commit }, projectIds) =>
commit(types.SET_SELECTED_PROJECTS, projectIds);
......@@ -44,14 +38,19 @@ export const receiveStageDataError = ({ commit }) => {
createFlash(__('There was an error fetching data for the selected stage'));
};
export const fetchStageData = ({ state, dispatch, getters }) => {
export const fetchStageData = ({ state, dispatch, getters }, slug) => {
const { cycleAnalyticsRequestParams = {} } = getters;
dispatch('requestStageData');
axios
.get(state.endpoints.stageData, {
params: nestQueryStringKeys(cycleAnalyticsRequestParams, 'cycle_analytics'),
})
const {
selectedGroup: { fullPath },
} = state;
return Api.cycleAnalyticsStageEvents(
fullPath,
slug,
nestQueryStringKeys(cycleAnalyticsRequestParams, 'cycle_analytics'),
)
.then(({ data }) => dispatch('receiveStageDataSuccess', data))
.catch(error => dispatch('receiveStageDataError', error));
};
......@@ -94,10 +93,14 @@ export const fetchSummaryData = ({ state, dispatch, getters }) => {
const { cycleAnalyticsRequestParams = {} } = getters;
dispatch('requestSummaryData');
return axios
.get(state.endpoints.cycleAnalyticsData, {
params: nestQueryStringKeys(cycleAnalyticsRequestParams, 'cycle_analytics'),
})
const {
selectedGroup: { fullPath },
} = state;
return Api.cycleAnalyticsSummaryData(
fullPath,
nestQueryStringKeys(cycleAnalyticsRequestParams, 'cycle_analytics'),
)
.then(({ data }) => dispatch('receiveSummaryDataSuccess', data))
.catch(error => dispatch('receiveSummaryDataError', error));
};
......@@ -139,23 +142,26 @@ export const receiveGroupStagesAndEventsSuccess = ({ state, commit, dispatch },
const { stages = [] } = state;
if (stages && stages.length) {
const { slug } = stages[0];
dispatch('setStageDataEndpoint', slug);
dispatch('fetchStageData');
dispatch('fetchStageData', slug);
} else {
createFlash(__('There was an error while fetching cycle analytics data.'));
}
};
export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
const {
selectedGroup: { fullPath },
} = state;
const {
cycleAnalyticsRequestParams: { created_after, project_ids },
} = getters;
dispatch('requestGroupStagesAndEvents');
return axios
.get(state.endpoints.cycleAnalyticsStagesAndEvents, {
params: nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'),
})
return Api.cycleAnalyticsGroupStagesAndEvents(
fullPath,
nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'),
)
.then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data))
.catch(error => dispatch('receiveGroupStagesAndEventsError', error));
};
......@@ -173,6 +179,8 @@ export const receiveCreateCustomStageError = ({ commit }, { error, data }) => {
const { name } = data;
const { status } = error;
// TODO: check for 403, 422 etc
// Follow up issue to investigate https://gitlab.com/gitlab-org/gitlab/issues/36685
const message =
status !== httpStatus.UNPROCESSABLE_ENTITY
? __(`'${name}' stage already exists'`)
......@@ -186,11 +194,9 @@ export const createCustomStage = ({ dispatch, state }, data) => {
selectedGroup: { fullPath },
} = state;
const endpoint = `/-/analytics/cycle_analytics/stages?group_id=${fullPath}`;
dispatch('requestCreateCustomStage');
axios
.post(endpoint, data)
return Api.cycleAnalyticsCreateStage(fullPath, data)
.then(response => dispatch('receiveCreateCustomStageSuccess', response))
.catch(error => dispatch('receiveCreateCustomStageError', { error, data }));
};
......
export const SET_CYCLE_ANALYTICS_DATA_ENDPOINT = 'SET_CYCLE_ANALYTICS_DATA_ENDPOINT';
export const SET_STAGE_DATA_ENDPOINT = 'SET_STAGE_DATA_ENDPOINT';
export const SET_SELECTED_GROUP = 'SET_SELECTED_GROUP';
export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_STAGE_ID = 'SET_SELECTED_STAGE_ID';
......
......@@ -3,18 +3,6 @@ import * as types from './mutation_types';
import { transformRawStages } from '../utils';
export default {
[types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT](state, groupPath) {
// TODO: this endpoint will be removed when the /-/analytics endpoints are ready
// https://gitlab.com/gitlab-org/gitlab/issues/34751
state.endpoints.cycleAnalyticsData = `/groups/${groupPath}/-/cycle_analytics`;
state.endpoints.cycleAnalyticsStagesAndEvents = `/-/analytics/cycle_analytics/stages?group_id=${groupPath}`;
},
[types.SET_STAGE_DATA_ENDPOINT](state, stageSlug) {
// TODO: this endpoint will be replaced with a /-/analytics... endpoint when backend is ready
// https://gitlab.com/gitlab-org/gitlab/issues/34751
const { fullPath } = state.selectedGroup;
state.endpoints.stageData = `/groups/${fullPath}/-/cycle_analytics/events/${stageSlug}.json`;
},
[types.SET_SELECTED_GROUP](state, group) {
state.selectedGroup = convertObjectPropsToCamelCase(group, { deep: true });
state.selectedProjectIds = [];
......
import { TASKS_BY_TYPE_SUBJECT_ISSUE } from '../constants';
export default () => ({
endpoints: {
cycleAnalyticsData: null,
stageData: null,
cycleAnalyticsStagesAndEvents: null,
summaryData: null,
},
startDate: null,
endDate: null,
......
......@@ -19,6 +19,9 @@ export default {
projectPackagesPath: '/api/:version/projects/:id/packages',
projectPackagePath: '/api/:version/projects/:id/packages/:package_id',
cycleAnalyticsTasksByTypePath: '/-/analytics/type_of_work/tasks_by_type',
cycleAnalyticsSummaryDataPath: '/groups/:group_id/-/cycle_analytics',
cycleAnalyticsGroupStagesAndEventsPath: '/-/analytics/cycle_analytics/stages',
cycleAnalyticsStageEventsPath: '/groups/:group_id/-/cycle_analytics/events/:stage_id.json',
userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
......@@ -141,4 +144,33 @@ export default {
const url = Api.buildUrl(this.cycleAnalyticsTasksByTypePath);
return axios.get(url, { params });
},
cycleAnalyticsSummaryData(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsSummaryDataPath).replace(':group_id', groupId);
return axios.get(url, { params });
},
cycleAnalyticsGroupStagesAndEvents(groupId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath);
return axios.get(url, {
params: { group_id: groupId, ...params },
});
},
cycleAnalyticsStageEvents(groupId, stageId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsStageEventsPath)
.replace(':group_id', groupId)
.replace(':stage_id', stageId);
return axios.get(url, { params });
},
cycleAnalyticsCreateStage(groupId, data) {
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath);
return axios.post(url, data, {
params: { group_id: groupId },
});
},
};
......@@ -309,7 +309,7 @@ describe('Cycle Analytics component', () => {
},
fetchGroupStagesAndEvents: {
status: defaultStatus,
endpoint: `/-/analytics/cycle_analytics/stages?group_id=${groupId}`,
endpoint: `/-/analytics/cycle_analytics/stages`,
response: { ...mockData.customizableStagesAndEvents },
},
fetchGroupLabels: {
......
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 createFlash from '~/flash';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import * as actions from 'ee/analytics/cycle_analytics/store/actions';
......@@ -18,10 +17,14 @@ import {
const stageData = { events: [] };
const error = new Error('Request failed with status code 404');
const groupPath = 'cool-group';
const groupLabelsEndpoint = `/groups/${groupPath}/-/labels`;
const flashErrorMessage = 'There was an error while fetching cycle analytics data.';
const selectedGroup = { fullPath: groupPath };
const selectedGroup = { fullPath: group.path };
const [{ id: selectedStageSlug }] = stages;
const endpoints = {
groupLabels: `/groups/${group.path}/-/labels`,
cycleAnalyticsData: `/groups/${group.path}/-/cycle_analytics`,
stageData: `/groups/${group.path}/-/cycle_analytics/events/${selectedStageSlug}.json`,
};
describe('Cycle analytics actions', () => {
let state;
......@@ -33,10 +36,6 @@ describe('Cycle analytics actions', () => {
beforeEach(() => {
state = {
endpoints: {
cycleAnalyticsData: `${TEST_HOST}/groups/${group.path}/-/cycle_analytics`,
stageData: `${TEST_HOST}/groups/${group.path}/-/cycle_analytics/events/${cycleAnalyticsData.stats[0].name}.json`,
},
stages: [],
getters,
};
......@@ -49,12 +48,10 @@ describe('Cycle analytics actions', () => {
});
it.each`
action | type | stateKey | payload
${'setCycleAnalyticsDataEndpoint'} | ${'SET_CYCLE_ANALYTICS_DATA_ENDPOINT'} | ${'endpoints.cycleAnalyticsData'} | ${'coolGroupName'}
${'setStageDataEndpoint'} | ${'SET_STAGE_DATA_ENDPOINT'} | ${'endpoints.stageData'} | ${'new_stage_name'}
${'setSelectedGroup'} | ${'SET_SELECTED_GROUP'} | ${'selectedGroup'} | ${'someNewGroup'}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStageId'} | ${'SET_SELECTED_STAGE_ID'} | ${'selectedStageId'} | ${'someNewGroup'}
action | type | stateKey | payload
${'setSelectedGroup'} | ${'SET_SELECTED_GROUP'} | ${'selectedGroup'} | ${'someNewGroup'}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStageId'} | ${'SET_SELECTED_STAGE_ID'} | ${'selectedStageId'} | ${'someNewGroup'}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
testAction(
actions[action],
......@@ -87,13 +84,14 @@ describe('Cycle analytics actions', () => {
describe('fetchStageData', () => {
beforeEach(() => {
mock.onGet(state.endpoints.stageData).replyOnce(200, { events: [] });
state = { ...state, selectedGroup };
mock.onGet(endpoints.stageData).replyOnce(200, { events: [] });
});
it('dispatches receiveStageDataSuccess with received data on success', done => {
testAction(
actions.fetchStageData,
null,
selectedStageSlug,
state,
[],
[
......@@ -108,17 +106,10 @@ describe('Cycle analytics actions', () => {
});
it('dispatches receiveStageDataError on error', done => {
const brokenState = {
...state,
endpoints: {
stageData: 'this will break',
},
};
testAction(
actions.fetchStageData,
null,
brokenState,
state,
[],
[
{ type: 'requestStageData' },
......@@ -176,7 +167,7 @@ describe('Cycle analytics actions', () => {
describe('fetchGroupLabels', () => {
beforeEach(() => {
state = { ...state, selectedGroup };
mock.onGet(groupLabelsEndpoint).replyOnce(200, groupLabels);
mock.onGet(endpoints.groupLabels).replyOnce(200, groupLabels);
});
it('dispatches receiveGroupLabels if the request succeeds', done => {
......@@ -251,7 +242,7 @@ describe('Cycle analytics actions', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock.onGet(state.endpoints.cycleAnalyticsData).replyOnce(200, cycleAnalyticsData);
mock.onGet(endpoints.cycleAnalyticsData).replyOnce(200, cycleAnalyticsData);
state = { ...state, selectedGroup, startDate, endDate };
});
......@@ -354,8 +345,7 @@ describe('Cycle analytics actions', () => {
});
});
it("dispatches the 'setStageDataEndpoint' and 'fetchStageData' actions", done => {
const { id } = stages[0];
it("dispatches the 'fetchStageData' action", done => {
const stateWithStages = {
...state,
stages,
......@@ -371,7 +361,7 @@ describe('Cycle analytics actions', () => {
payload: { ...customizableStagesAndEvents },
},
],
[{ type: 'setStageDataEndpoint', payload: id }, { type: 'fetchStageData' }],
[{ type: 'fetchStageData', payload: selectedStageSlug }],
done,
);
});
......@@ -463,8 +453,7 @@ describe('Cycle analytics actions', () => {
);
});
it("dispatches the 'setStageDataEndpoint' and 'fetchStageData' actions", done => {
const { id } = stages[0];
it("dispatches the 'fetchStageData' actions", done => {
const stateWithStages = {
...state,
stages,
......@@ -480,7 +469,7 @@ describe('Cycle analytics actions', () => {
payload: { ...customizableStagesAndEvents },
},
],
[{ type: 'setStageDataEndpoint', payload: id }, { type: 'fetchStageData' }],
[{ type: 'fetchStageData', payload: selectedStageSlug }],
done,
);
});
......
......@@ -57,13 +57,11 @@ describe('Cycle analytics mutations', () => {
});
it.each`
mutation | payload | expectedState
${types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT} | ${'cool-beans'} | ${{ endpoints: { cycleAnalyticsStagesAndEvents: '/-/analytics/cycle_analytics/stages?group_id=cool-beans' } }}
${types.SET_STAGE_DATA_ENDPOINT} | ${'rad-stage'} | ${{ endpoints: { stageData: '/groups/rad-stage/-/cycle_analytics/events/rad-stage.json' } }}
${types.SET_SELECTED_GROUP} | ${{ fullPath: 'cool-beans' }} | ${{ selectedGroup: { fullPath: 'cool-beans' }, selectedProjectIds: [] }}
${types.SET_SELECTED_PROJECTS} | ${[606, 707, 808, 909]} | ${{ selectedProjectIds: [606, 707, 808, 909] }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE_ID} | ${'first-stage'} | ${{ selectedStageId: 'first-stage' }}
mutation | payload | expectedState
${types.SET_SELECTED_GROUP} | ${{ fullPath: 'cool-beans' }} | ${{ selectedGroup: { fullPath: 'cool-beans' }, selectedProjectIds: [] }}
${types.SET_SELECTED_PROJECTS} | ${[606, 707, 808, 909]} | ${{ selectedProjectIds: [606, 707, 808, 909] }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE_ID} | ${'first-stage'} | ${{ selectedStageId: 'first-stage' }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......
......@@ -289,6 +289,17 @@ describe('Api', () => {
const groupId = 'counting-54321';
const createdBefore = '2019-11-18';
const createdAfter = '2019-08-18';
const stageId = 'thursday';
const expectRequestWithCorrectParameters = (responseObj, { params, expectedUrl, response }) => {
const {
data,
config: { params: reqParams, url },
} = responseObj;
expect(data).toEqual(response);
expect(reqParams).toEqual(params);
expect(url).toEqual(expectedUrl);
};
describe('cycleAnalyticsTasksByType', () => {
it('fetches tasks by type data', done => {
......@@ -330,5 +341,101 @@ describe('Api', () => {
.catch(done.fail);
});
});
describe('cycleAnalyticsSummaryData', () => {
it('fetches cycle analytics summary, stage stats and permissions data', done => {
const response = { summary: [], stats: [], permissions: {} };
const params = {
'cycle_analytics[created_after]': createdAfter,
'cycle_analytics[created_before]': createdBefore,
};
const expectedUrl = `${dummyUrlRoot}/groups/${groupId}/-/cycle_analytics`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsSummaryData(groupId, params)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
params,
expectedUrl,
}),
)
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsGroupStagesAndEvents', () => {
it('fetches custom stage events and all stages', done => {
const response = { events: [], stages: [] };
const params = {
group_id: groupId,
'cycle_analytics[created_after]': createdAfter,
'cycle_analytics[created_before]': createdBefore,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/cycle_analytics/stages`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsGroupStagesAndEvents(groupId, params)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
params,
expectedUrl,
}),
)
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsStageEvents', () => {
it('fetches stage events', done => {
const response = { events: [] };
const params = {
'cycle_analytics[group_id]': groupId,
'cycle_analytics[created_after]': createdAfter,
'cycle_analytics[created_before]': createdBefore,
};
const expectedUrl = `${dummyUrlRoot}/groups/${groupId}/-/cycle_analytics/events/${stageId}.json`;
mock.onGet(expectedUrl).reply(200, response);
Api.cycleAnalyticsStageEvents(groupId, stageId, params)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
params,
expectedUrl,
}),
)
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsCreateStage', () => {
it('submit the custom stage data', done => {
const response = {};
const customStage = {
name: 'cool-stage',
start_event_identifier: 'issue_created',
start_event_label_id: null,
end_event_identifier: 'issue_closed',
end_event_label_id: null,
};
const expectedUrl = `${dummyUrlRoot}/-/analytics/cycle_analytics/stages`;
mock.onPost(expectedUrl).reply(200, response);
Api.cycleAnalyticsCreateStage(groupId, customStage)
.then(({ data, config: { params: reqParams, data: reqData, url } }) => {
expect(data).toEqual(response);
expect(reqParams).toEqual({ group_id: groupId });
expect(JSON.parse(reqData)).toMatchObject(customStage);
expect(url).toEqual(expectedUrl);
})
.then(done)
.catch(done.fail);
});
});
});
});
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