Commit f57e8d2b authored by Fatih Acet's avatar Fatih Acet

Merge branch '35206-follow-up-consolidate-cycle-analytics-urls' into 'master'

Follow up: consolidate cycle analytics URLs

Closes #35206

See merge request gitlab-org/gitlab!20192
parents b0182221 3894edef
......@@ -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