Commit 509daefb authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '273107-refactor-vsa-vuex-root-actions' into 'master'

[VSA] Split Vuex actions into separate files

See merge request gitlab-org/gitlab!67113
parents 05476fef 1e511a25
import * as types from '../mutation_types';
const refreshData = ({ selectedStage, isOverviewStageSelected, dispatch }) => {
if (selectedStage && !isOverviewStageSelected) dispatch('fetchStageData', selectedStage.id);
return dispatch('fetchCycleAnalyticsData');
};
export const setSelectedProjects = (
{ commit, dispatch, getters: { isOverviewStageSelected }, state: { selectedStage } },
projects,
) => {
commit(types.SET_SELECTED_PROJECTS, projects);
return refreshData({ dispatch, selectedStage, isOverviewStageSelected });
};
export const setDateRange = (
{ commit, dispatch, getters: { isOverviewStageSelected }, state: { selectedStage } },
{ createdAfter, createdBefore },
) => {
commit(types.SET_DATE_RANGE, { createdBefore, createdAfter });
if (selectedStage && !isOverviewStageSelected) dispatch('fetchStageData', selectedStage.id);
return dispatch('fetchCycleAnalyticsData');
};
export const setFilters = ({
dispatch,
getters: { isOverviewStageSelected },
state: { selectedStage },
}) => {
return refreshData({ dispatch, isOverviewStageSelected, selectedStage });
};
export const updateStageTablePagination = (
{ commit, dispatch, state: { selectedStage } },
paginationParams,
) => {
commit(types.SET_PAGINATION, paginationParams);
return dispatch('fetchStageData', selectedStage.id);
};
import Api from 'ee/api';
import { getValueStreamStageMedian } from '~/api/analytics_api';
import createFlash from '~/flash';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { OVERVIEW_STAGE_CONFIG } from '../../constants';
import { checkForDataError, flashErrorIfStatusNotOk, throwIfUserForbidden } from '../../utils';
import * as types from '../mutation_types';
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
export const setDefaultSelectedStage = ({ dispatch }) =>
dispatch('setSelectedStage', OVERVIEW_STAGE_CONFIG);
export const requestStageData = ({ commit }) => commit(types.REQUEST_STAGE_DATA);
export const receiveStageDataError = ({ commit }, error) => {
const { message = '' } = error;
flashErrorIfStatusNotOk({
error,
message: __('There was an error fetching data for the selected stage'),
});
commit(types.RECEIVE_STAGE_DATA_ERROR, message);
};
export const fetchStageData = ({ dispatch, getters, commit }, stageId) => {
const {
cycleAnalyticsRequestParams = {},
currentValueStreamId,
currentGroupPath,
paginationParams,
} = getters;
dispatch('requestStageData');
return Api.cycleAnalyticsStageEvents({
groupId: currentGroupPath,
valueStreamId: currentValueStreamId,
stageId,
params: {
...cycleAnalyticsRequestParams,
...paginationParams,
},
})
.then(checkForDataError)
.then(({ data, headers }) => {
const { page = null, nextPage = null } = parseIntPagination(normalizeHeaders(headers));
commit(types.RECEIVE_STAGE_DATA_SUCCESS, data);
commit(types.SET_PAGINATION, { ...paginationParams, page, hasNextPage: Boolean(nextPage) });
})
.catch((error) => dispatch('receiveStageDataError', error));
};
export const requestStageMedianValues = ({ commit }) => commit(types.REQUEST_STAGE_MEDIANS);
export const receiveStageMedianValuesError = ({ commit }, error) => {
commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
createFlash({
message: __('There was an error fetching median data for stages'),
});
};
const fetchStageMedian = ({ groupId, valueStreamId, stageId, params }) =>
getValueStreamStageMedian({ groupId, valueStreamId, stageId }, params).then(({ data }) => {
return {
id: stageId,
...(data?.error
? {
error: data.error,
value: null,
}
: data),
};
});
export const fetchStageMedianValues = ({ dispatch, commit, getters }) => {
const {
currentGroupPath,
cycleAnalyticsRequestParams,
activeStages,
currentValueStreamId,
} = getters;
const stageIds = activeStages.map((s) => s.slug);
dispatch('requestStageMedianValues');
return Promise.all(
stageIds.map((stageId) =>
fetchStageMedian({
groupId: currentGroupPath,
valueStreamId: currentValueStreamId,
stageId,
params: cycleAnalyticsRequestParams,
}),
),
)
.then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
.catch((error) => dispatch('receiveStageMedianValuesError', error));
};
const fetchStageCount = ({ groupId, valueStreamId, stageId, params }) =>
Api.cycleAnalyticsStageCount({ groupId, valueStreamId, stageId, params }).then(({ data }) => {
return {
id: stageId,
...(data?.error
? {
error: data.error,
value: null,
}
: data),
};
});
export const fetchStageCountValues = ({ commit, getters }) => {
const {
currentGroupPath,
cycleAnalyticsRequestParams,
activeStages,
currentValueStreamId,
} = getters;
const stageIds = activeStages.map((s) => s.slug);
commit(types.REQUEST_STAGE_COUNTS);
return Promise.all(
stageIds.map((stageId) =>
fetchStageCount({
groupId: currentGroupPath,
valueStreamId: currentValueStreamId,
stageId,
params: cycleAnalyticsRequestParams,
}),
),
)
.then((data) => commit(types.RECEIVE_STAGE_COUNTS_SUCCESS, data))
.catch((error) => commit(types.RECEIVE_STAGE_COUNTS_ERROR, error));
};
export const requestGroupStages = ({ commit }) => commit(types.REQUEST_GROUP_STAGES);
export const receiveGroupStagesError = ({ commit }, error) => {
commit(types.RECEIVE_GROUP_STAGES_ERROR, error);
createFlash({
message: __('There was an error fetching value stream analytics stages.'),
});
};
export const receiveGroupStagesSuccess = ({ commit }, stages) =>
commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages);
export const fetchGroupStagesAndEvents = ({ dispatch, commit, getters }) => {
const {
currentValueStreamId: valueStreamId,
currentGroupPath: groupId,
cycleAnalyticsRequestParams: { created_after, project_ids },
} = getters;
dispatch('requestGroupStages');
commit(types.SET_STAGE_EVENTS, []);
return Api.cycleAnalyticsGroupStagesAndEvents({
groupId,
valueStreamId,
params: {
start_date: created_after,
project_ids,
},
})
.then(({ data: { stages = [], events = [] } }) => {
dispatch('receiveGroupStagesSuccess', stages);
commit(types.SET_STAGE_EVENTS, events);
})
.catch((error) => {
throwIfUserForbidden(error);
return dispatch('receiveGroupStagesError', error);
});
};
import Api from 'ee/api';
import { FETCH_VALUE_STREAM_DATA } from '../../constants';
import * as types from '../mutation_types';
export const receiveCreateValueStreamSuccess = ({ commit, dispatch }, valueStream = {}) => {
commit(types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, valueStream);
return dispatch('fetchCycleAnalyticsData');
};
export const createValueStream = ({ commit, dispatch, getters }, data) => {
const { currentGroupPath } = getters;
commit(types.REQUEST_CREATE_VALUE_STREAM);
return Api.cycleAnalyticsCreateValueStream(currentGroupPath, data)
.then(({ data: newValueStream }) => dispatch('receiveCreateValueStreamSuccess', newValueStream))
.catch(({ response } = {}) => {
const { data: { message, payload: { errors } } = null } = response;
commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors, data });
});
};
export const updateValueStream = (
{ commit, dispatch, getters },
{ id: valueStreamId, ...data },
) => {
const { currentGroupPath } = getters;
commit(types.REQUEST_UPDATE_VALUE_STREAM);
return Api.cycleAnalyticsUpdateValueStream({ groupId: currentGroupPath, valueStreamId, data })
.then(({ data: newValueStream }) => {
commit(types.RECEIVE_UPDATE_VALUE_STREAM_SUCCESS, newValueStream);
return dispatch('fetchCycleAnalyticsData');
})
.catch(({ response } = {}) => {
const { data: { message, payload: { errors } } = null } = response;
commit(types.RECEIVE_UPDATE_VALUE_STREAM_ERROR, { message, errors, data });
});
};
export const deleteValueStream = ({ commit, dispatch, getters }, valueStreamId) => {
const { currentGroupPath } = getters;
commit(types.REQUEST_DELETE_VALUE_STREAM);
return Api.cycleAnalyticsDeleteValueStream(currentGroupPath, valueStreamId)
.then(() => commit(types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS))
.then(() => dispatch('fetchCycleAnalyticsData'))
.catch(({ response } = {}) => {
const { data: { message } = null } = response;
commit(types.RECEIVE_DELETE_VALUE_STREAM_ERROR, message);
});
};
export const fetchValueStreamData = ({ dispatch }) =>
Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('durationChart/fetchDurationData'));
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return dispatch(FETCH_VALUE_STREAM_DATA);
};
export const receiveValueStreamsSuccess = (
{ state: { selectedValueStream = null }, commit, dispatch },
data = [],
) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
if (!selectedValueStream && data.length) {
const [firstStream] = data;
return Promise.resolve()
.then(() => dispatch('setSelectedValueStream', firstStream))
.then(() => dispatch('fetchStageCountValues'));
}
return Promise.resolve()
.then(() => dispatch(FETCH_VALUE_STREAM_DATA))
.then(() => dispatch('fetchStageCountValues'));
};
export const fetchValueStreams = ({ commit, dispatch, getters }) => {
const { currentGroupPath } = getters;
commit(types.REQUEST_VALUE_STREAMS);
return Api.cycleAnalyticsValueStreams(currentGroupPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.catch((error) => {
const {
response: { status },
} = error;
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
throw error;
});
};
import * as actions from 'ee/analytics/cycle_analytics/store/actions/filters';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { createdAfter, createdBefore, selectedProjects } from 'jest/cycle_analytics/mock_data';
import { allowedStages as stages } from '../../mock_data';
stages[0].hidden = true;
const activeStages = stages.filter(({ hidden }) => !hidden);
const [selectedStage] = activeStages;
describe('Value Stream Analytics actions / filters', () => {
let state;
let stateWithOverview = null;
describe.each`
targetAction | payload | mutations
${actions.setDateRange} | ${{ createdAfter, createdBefore }} | ${[{ type: 'SET_DATE_RANGE', payload: { createdAfter, createdBefore } }]}
${actions.setFilters} | ${''} | ${[]}
`('$action', ({ targetAction, payload, mutations }) => {
beforeEach(() => {
stateWithOverview = { ...state, isOverviewStageSelected: () => true };
});
it('dispatches the fetchCycleAnalyticsData action', () => {
return testAction(targetAction, payload, stateWithOverview, mutations, [
{ type: 'fetchCycleAnalyticsData' },
]);
});
describe('with a stage selected', () => {
beforeEach(() => {
stateWithOverview = { ...state, selectedStage };
});
it('dispatches the fetchStageData action', () => {
return testAction(targetAction, payload, stateWithOverview, mutations, [
{ type: 'fetchStageData', payload: selectedStage.id },
{ type: 'fetchCycleAnalyticsData' },
]);
});
});
});
describe('setSelectedProjects', () => {
describe('with `overview` stage selected', () => {
beforeEach(() => {
stateWithOverview = { ...state, isOverviewStageSelected: () => true };
});
it('will dispatch the "fetchCycleAnalyticsData" action', () => {
return testAction(
actions.setSelectedProjects,
selectedProjects,
stateWithOverview,
[{ type: types.SET_SELECTED_PROJECTS, payload: selectedProjects }],
[{ type: 'fetchCycleAnalyticsData' }],
);
});
});
describe('with non overview stage selected', () => {
beforeEach(() => {
state = { ...state, selectedStage };
});
it('will dispatch the "fetchStageData" and "fetchCycleAnalyticsData" actions', () => {
return testAction(
actions.setSelectedProjects,
selectedProjects,
state,
[{ type: types.SET_SELECTED_PROJECTS, payload: selectedProjects }],
[
{ type: 'fetchStageData', payload: selectedStage.id },
{ type: 'fetchCycleAnalyticsData' },
],
);
});
});
});
});
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { OVERVIEW_STAGE_CONFIG } from 'ee/analytics/cycle_analytics/constants';
import * as actions from 'ee/analytics/cycle_analytics/store/actions/stages';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import testAction from 'helpers/vuex_action_helper';
import { createdAfter, createdBefore, currentGroup } from 'jest/cycle_analytics/mock_data';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import {
allowedStages as stages,
customizableStagesAndEvents,
endpoints,
valueStreams,
} from '../../mock_data';
const stageData = { events: [] };
const error = new Error(`Request failed with status code ${httpStatusCodes.NOT_FOUND}`);
stages[0].hidden = true;
const activeStages = stages.filter(({ hidden }) => !hidden);
const hiddenStage = stages[0];
const [selectedStage] = activeStages;
const selectedStageSlug = selectedStage.slug;
const [selectedValueStream] = valueStreams;
const mockGetters = {
currentGroupPath: () => currentGroup.fullPath,
currentValueStreamId: () => selectedValueStream.id,
};
jest.mock('~/flash');
describe('Value Stream Analytics actions / stages', () => {
let state;
let mock;
beforeEach(() => {
state = {
createdAfter,
createdBefore,
stages: [],
featureFlags: {},
activeStages,
selectedValueStream,
...mockGetters,
};
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
state = { ...state, currentGroup: null };
});
describe('setSelectedStage', () => {
const data = { id: 'someStageId' };
it(`dispatches the ${types.SET_SELECTED_STAGE} and ${types.SET_PAGINATION} actions`, () => {
return testAction(actions.setSelectedStage, data, { ...state, selectedValueStream: {} }, [
{ type: types.SET_SELECTED_STAGE, payload: data },
]);
});
});
describe('setDefaultSelectedStage', () => {
it("dispatches the 'setSelectedStage' with the overview stage", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
state,
[],
[{ type: 'setSelectedStage', payload: OVERVIEW_STAGE_CONFIG }],
);
});
});
describe('fetchStageData', () => {
const headers = {
'X-Next-Page': 2,
'X-Page': 1,
};
beforeEach(() => {
state = { ...state, currentGroup };
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageData).reply(httpStatusCodes.OK, stageData, headers);
});
it(`commits ${types.RECEIVE_STAGE_DATA_SUCCESS} with received data and headers on success`, () => {
return testAction(
actions.fetchStageData,
selectedStageSlug,
state,
[
{
type: types.RECEIVE_STAGE_DATA_SUCCESS,
payload: stageData,
},
{
type: types.SET_PAGINATION,
payload: { page: headers['X-Page'], hasNextPage: true },
},
],
[{ type: 'requestStageData' }],
);
});
describe('without a next page', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock
.onGet(endpoints.stageData)
.reply(httpStatusCodes.OK, { events: [] }, { ...headers, 'X-Next-Page': null });
});
it('sets hasNextPage to false', () => {
return testAction(
actions.fetchStageData,
selectedStageSlug,
state,
[
{
type: types.RECEIVE_STAGE_DATA_SUCCESS,
payload: { events: [] },
},
{
type: types.SET_PAGINATION,
payload: { page: headers['X-Page'], hasNextPage: false },
},
],
[{ type: 'requestStageData' }],
);
});
});
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageData).replyOnce(httpStatusCodes.NOT_FOUND, { error });
});
it('dispatches receiveStageDataError on error', () => {
return testAction(
actions.fetchStageData,
selectedStage,
state,
[],
[
{
type: 'requestStageData',
},
{
type: 'receiveStageDataError',
payload: error,
},
],
);
});
});
});
describe('receiveStageDataError', () => {
const message = 'fake error';
it(`commits the ${types.RECEIVE_STAGE_DATA_ERROR} mutation`, () => {
return testAction(
actions.receiveStageDataError,
{ message },
state,
[
{
type: types.RECEIVE_STAGE_DATA_ERROR,
payload: message,
},
],
[],
);
});
it('will flash an error message', () => {
actions.receiveStageDataError({ commit: () => {} }, {});
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching data for the selected stage',
});
});
});
describe('fetchStageMedianValues', () => {
let mockDispatch = jest.fn();
const fetchMedianResponse = activeStages.map(({ slug: id }) => ({ events: [], id }));
beforeEach(() => {
state = { ...state, stages, currentGroup };
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.OK, { events: [] });
mockDispatch = jest.fn();
});
it('dispatches receiveStageMedianValuesSuccess with received data on success', () => {
return testAction(
actions.fetchStageMedianValues,
null,
state,
[{ type: types.RECEIVE_STAGE_MEDIANS_SUCCESS, payload: fetchMedianResponse }],
[{ type: 'requestStageMedianValues' }],
);
});
it('does not request hidden stages', () => {
return actions
.fetchStageMedianValues({
state,
getters: {
...getters,
activeStages,
},
commit: () => {},
dispatch: mockDispatch,
})
.then(() => {
expect(mockDispatch).not.toHaveBeenCalledWith('receiveStageMedianValuesSuccess', {
events: [],
id: hiddenStage.id,
});
});
});
describe(`Status ${httpStatusCodes.OK} and error message in response`, () => {
const dataError = 'Too much data';
const payload = activeStages.map(({ slug: id }) => ({ value: null, id, error: dataError }));
beforeEach(() => {
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.OK, { error: dataError });
});
it(`dispatches the 'RECEIVE_STAGE_MEDIANS_SUCCESS' with ${dataError}`, () => {
return testAction(
actions.fetchStageMedianValues,
null,
state,
[{ type: types.RECEIVE_STAGE_MEDIANS_SUCCESS, payload }],
[{ type: 'requestStageMedianValues' }],
);
});
});
describe('with a failing request', () => {
beforeEach(() => {
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.NOT_FOUND, { error });
});
it('will dispatch receiveStageMedianValuesError', () => {
return testAction(
actions.fetchStageMedianValues,
null,
state,
[],
[
{ type: 'requestStageMedianValues' },
{ type: 'receiveStageMedianValuesError', payload: error },
],
);
});
});
});
describe('receiveStageMedianValuesError', () => {
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () =>
testAction(
actions.receiveStageMedianValuesError,
{},
state,
[
{
type: types.RECEIVE_STAGE_MEDIANS_ERROR,
payload: {},
},
],
[],
));
it('will flash an error message', () => {
actions.receiveStageMedianValuesError({ commit: () => {} });
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching median data for stages',
});
});
});
describe('fetchStageCountValues', () => {
const fetchCountResponse = activeStages.map(({ slug: id }) => ({ events: [], id }));
beforeEach(() => {
state = {
...state,
stages,
currentGroup,
featureFlags: state.featureFlags,
};
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageCount).reply(httpStatusCodes.OK, { events: [] });
});
it('dispatches receiveStageCountValuesSuccess with received data on success', () => {
return testAction(
actions.fetchStageCountValues,
null,
state,
[
{ type: types.REQUEST_STAGE_COUNTS },
{ type: types.RECEIVE_STAGE_COUNTS_SUCCESS, payload: fetchCountResponse },
],
[],
);
});
});
describe('receiveGroupStagesSuccess', () => {
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[],
);
});
});
});
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