Commit ff4a37dc authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '326705-project-vsa-add-vuex-support-for-value-streams' into 'master'

[VSA][FE] Introduce a default value stream

See merge request gitlab-org/gitlab!63397
parents e64f72c9 26ba0b7c
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
if (valueStreamId) {
return buildApiUrl(PROJECT_VSA_STAGES_PATH)
.replace(':project_path', projectPath)
.replace(':value_stream_id', valueStreamId);
}
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
};
export const getProjectValueStreams = (projectPath) => {
const url = buildProjectValueStreamPath(projectPath);
return axios.get(url);
};
export const getProjectValueStreamStages = (projectPath, valueStreamId) => {
const url = buildProjectValueStreamPath(projectPath, valueStreamId);
return axios.get(url);
};
// NOTE: legacy VSA request use a different path
// the `requestPath` provides a full url for the request
export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) =>
axios.get(`${requestPath}/events/${stageId}`, { params });
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
......@@ -58,6 +58,7 @@ export default {
'stages',
'summary',
'startDate',
'permissions',
]),
...mapGetters(['pathNavigationData']),
displayStageEvents() {
......@@ -68,7 +69,7 @@ export default {
return this.selectedStageReady && this.isEmptyStage;
},
displayNoAccess() {
return this.selectedStageReady && !this.selectedStage.isUserAllowed;
return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id);
},
selectedStageReady() {
return !this.isLoadingStage && this.selectedStage;
......@@ -91,25 +92,18 @@ export default {
]),
handleDateSelect(startDate) {
this.setDateRange({ startDate });
this.fetchCycleAnalyticsData();
},
isActiveStage(stage) {
return stage.slug === this.selectedStage.slug;
},
onSelectStage(stage) {
if (this.isLoadingStage || this.selectedStage?.slug === stage?.slug) return;
this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
return;
}
this.fetchStageData();
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
},
isUserAllowed(id) {
const { permissions } = this;
return Boolean(permissions?.[id]);
},
},
dayRangeOptions: [7, 30, 90],
i18n: {
......
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
id: 'default',
slug: 'default',
name: 'default',
};
......@@ -8,10 +8,11 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
const { noAccessSvgPath, noDataSvgPath, requestPath } = el.dataset;
const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
store.dispatch('initializeVsa', {
requestPath,
fullPath,
});
// eslint-disable-next-line no-new
......@@ -24,6 +25,7 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
fullPath,
},
}),
});
......
import {
getProjectValueStreamStages,
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants';
import * as types from './mutation_types';
export const fetchCycleAnalyticsData = ({
state: { requestPath, startDate },
dispatch,
commit,
}) => {
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return dispatch('fetchValueStreamStages');
};
export const fetchValueStreamStages = ({ commit, state }) => {
const { fullPath, selectedValueStream } = state;
commit(types.REQUEST_VALUE_STREAM_STAGES);
return getProjectValueStreamStages(fullPath, selectedValueStream.id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAM_STAGES_ERROR, status);
});
};
export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
if (data.length) {
const [firstStream] = data;
return dispatch('setSelectedValueStream', firstStream);
}
return dispatch('setSelectedValueStream', DEFAULT_VALUE_STREAM);
};
export const fetchValueStreams = ({ commit, dispatch, state }) => {
const { fullPath } = state;
commit(types.REQUEST_VALUE_STREAMS);
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => dispatch('setSelectedStage'))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
return axios
.get(requestPath, {
params: { 'cycle_analytics[start_date]': startDate },
})
return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.then(() => dispatch('setSelectedStage'))
.then(() => dispatch('fetchStageData'))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
createFlash({
message: __('There was an error while fetching value stream analytics data.'),
message: __('There was an error while fetching value stream summary data.'),
});
});
};
......@@ -29,10 +62,11 @@ export const fetchCycleAnalyticsData = ({
export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
commit(types.REQUEST_STAGE_DATA);
return axios
.get(`${requestPath}/events/${selectedStage.name}.json`, {
params: { 'cycle_analytics[start_date]': startDate },
})
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
params: { 'cycle_analytics[start_date]': startDate },
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) {
......@@ -44,15 +78,26 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};
export const setSelectedStage = ({ commit, state: { stages } }, selectedStage = null) => {
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
return dispatch('fetchStageData');
};
const refetchData = (dispatch, commit) => {
commit(types.SET_LOADING, true);
return Promise.resolve()
.then(() => dispatch('fetchValueStreams'))
.then(() => dispatch('fetchCycleAnalyticsData'))
.finally(() => commit(types.SET_LOADING, false));
};
export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) =>
export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => {
commit(types.SET_DATE_RANGE, { startDate });
return refetchData(dispatch, commit);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
return dispatch('fetchCycleAnalyticsData');
return refetchData(dispatch, commit);
};
export const INITIALIZE_VSA = 'INITIALIZE_VSA';
export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const REQUEST_VALUE_STREAMS = 'REQUEST_VALUE_STREAMS';
export const RECEIVE_VALUE_STREAMS_SUCCESS = 'RECEIVE_VALUE_STREAMS_SUCCESS';
export const RECEIVE_VALUE_STREAMS_ERROR = 'RECEIVE_VALUE_STREAMS_ERROR';
export const REQUEST_VALUE_STREAM_STAGES = 'REQUEST_VALUE_STREAM_STAGES';
export const RECEIVE_VALUE_STREAM_STAGES_SUCCESS = 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS';
export const RECEIVE_VALUE_STREAM_STAGES_ERROR = 'RECEIVE_VALUE_STREAM_STAGES_ERROR';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
export const RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR';
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { decorateData, decorateEvents, formatMedianValues } from '../utils';
import * as types from './mutation_types';
export default {
[types.INITIALIZE_VSA](state, { requestPath }) {
[types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
state.requestPath = requestPath;
state.fullPath = fullPath;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
},
[types.SET_SELECTED_VALUE_STREAM](state, selectedValueStream = {}) {
state.selectedValueStream = convertObjectPropsToCamelCase(selectedValueStream, { deep: true });
},
[types.SET_SELECTED_STAGE](state, stage) {
state.isLoadingStage = true;
state.selectedStage = stage;
state.isLoadingStage = false;
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
},
[types.RECEIVE_VALUE_STREAMS_SUCCESS](state, valueStreams = []) {
state.valueStreams = valueStreams;
},
[types.RECEIVE_VALUE_STREAMS_ERROR](state) {
state.valueStreams = [];
},
[types.REQUEST_VALUE_STREAM_STAGES](state) {
state.stages = [];
},
[types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) {
state.stages = stages.map((s) => ({
...convertObjectPropsToCamelCase(s, { deep: true }),
// NOTE: we set the component type here to match the current behaviour
// this can be removed when we migrate to the update stage table
// https://gitlab.com/gitlab-org/gitlab/-/issues/326704
component: `stage-${s.id}-component`,
}));
},
[types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) {
state.stages = [];
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.stages = [];
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false;
const { stages, summary, medians } = decorateData(data);
state.stages = stages;
const { summary, medians } = decorateData(data);
state.permissions = data.permissions;
state.summary = summary;
state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.stages = [];
state.hasError = true;
},
[types.REQUEST_STAGE_DATA](state) {
......
......@@ -2,11 +2,14 @@ import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
stages: [],
summary: [],
analytics: [],
stats: [],
valueStreams: [],
selectedValueStream: {},
selectedStage: {},
selectedStageEvents: [],
selectedStageError: '',
......@@ -15,4 +18,5 @@ export default () => ({
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
});
......@@ -2,31 +2,9 @@ import { unescape } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { dasherize } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '../locale';
import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
issue: __(
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
),
plan: __(
'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
),
code: __(
'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
),
test: __(
'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
),
review: __(
'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
),
staging: __(
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
),
};
/**
* These `decorate` methods will be removed when me migrate to the
* new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704
......@@ -43,33 +21,12 @@ const mapToEvent = (event, stage) => {
export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage));
/*
* NOTE: We currently use the `name` field since the project level stages are in memory
* once we migrate to a default value stream https://gitlab.com/gitlab-org/gitlab/-/issues/326705
* we can use the `id` to identify which median we are using
*/
const mapToStage = (permissions, { name, ...rest }) => {
const slug = dasherize(name.toLowerCase());
return {
...rest,
name,
id: name,
slug,
active: false,
isUserAllowed: permissions[slug],
emptyStageText: EMPTY_STAGE_TEXTS[slug],
component: `stage-${slug}-component`,
};
};
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
const mapToMedians = ({ id, value }) => ({ id, value });
const mapToMedians = ({ name: id, value }) => ({ id, value });
export const decorateData = (data = {}) => {
const { permissions, stats, summary } = data;
const stages = stats?.map((item) => mapToStage(permissions, item)) || [];
const { stats: stages, summary } = data;
return {
stages,
summary: summary?.map((item) => mapToSummary(item)) || [],
medians: stages?.map((item) => mapToMedians(item)) || [],
};
......
- page_title _("Value Stream Analytics")
- add_page_specific_style 'page_bundles/cycle_analytics'
- svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
- initial_data = { request_path: project_cycle_analytics_path(@project) }.merge!(svgs)
- initial_data = { request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
#js-cycle-analytics{ data: initial_data }
......@@ -32967,6 +32967,9 @@ msgstr ""
msgid "There was an error while fetching value stream analytics duration data."
msgstr ""
msgid "There was an error while fetching value stream summary data."
msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
......
......@@ -19,6 +19,9 @@ function createStore({ initialState = {} }) {
return new Vuex.Store({
state: {
...initState(),
permissions: {
[selectedStage.id]: true,
},
...initialState,
},
getters: {
......@@ -155,7 +158,11 @@ describe('Value stream analytics component', () => {
describe('without enough permissions', () => {
beforeEach(() => {
wrapper = createComponent({
initialState: { selectedStage: { ...selectedStage, isUserAllowed: false } },
initialState: {
permissions: {
[selectedStage.id]: false,
},
},
});
});
......
import { DEFAULT_VALUE_STREAM } from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export const getStageByTitle = (stages, title) =>
......@@ -95,54 +96,6 @@ export const rawData = {
};
export const convertedData = {
stages: [
selectedStage,
{
...planStage,
active: false,
isUserAllowed: true,
emptyStageText:
'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
component: 'stage-plan-component',
slug: 'plan',
},
{
...codeStage,
active: false,
isUserAllowed: true,
emptyStageText:
'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
component: 'stage-code-component',
slug: 'code',
},
{
...testStage,
active: false,
isUserAllowed: true,
emptyStageText:
'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
component: 'stage-test-component',
slug: 'test',
},
{
...reviewStage,
active: false,
isUserAllowed: true,
emptyStageText:
'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
component: 'stage-review-component',
slug: 'review',
},
{
...stagingStage,
active: false,
isUserAllowed: true,
emptyStageText:
'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
component: 'stage-staging-component',
slug: 'staging',
},
],
summary: [
{ value: '20', title: 'New Issues' },
{ value: '-', title: 'Commits' },
......@@ -256,3 +209,49 @@ export const transformedProjectStagePathData = [
value: 172800,
},
];
export const selectedValueStream = DEFAULT_VALUE_STREAM;
export const rawValueStreamStages = [
{
title: 'Issue',
hidden: false,
legend: '',
description: 'Time before an issue gets scheduled',
id: 'issue',
custom: false,
start_event_html_description:
'\u003cp data-sourcepos="1:1-1:13" dir="auto"\u003eIssue created\u003c/p\u003e',
end_event_html_description:
'\u003cp data-sourcepos="1:1-1:71" dir="auto"\u003eIssue first associated with a milestone or issue first added to a board\u003c/p\u003e',
},
{
title: 'Plan',
hidden: false,
legend: '',
description: 'Time before an issue starts implementation',
id: 'plan',
custom: false,
start_event_html_description:
'\u003cp data-sourcepos="1:1-1:71" dir="auto"\u003eIssue first associated with a milestone or issue first added to a board\u003c/p\u003e',
end_event_html_description:
'\u003cp data-sourcepos="1:1-1:33" dir="auto"\u003eIssue first mentioned in a commit\u003c/p\u003e',
},
{
title: 'Code',
hidden: false,
legend: '',
description: 'Time until first merge request',
id: 'code',
custom: false,
start_event_html_description:
'\u003cp data-sourcepos="1:1-1:33" dir="auto"\u003eIssue first mentioned in a commit\u003c/p\u003e',
end_event_html_description:
'\u003cp data-sourcepos="1:1-1:21" dir="auto"\u003eMerge request created\u003c/p\u003e',
},
];
export const valueStreamStages = rawValueStreamStages.map((s) => ({
...convertObjectPropsToCamelCase(s, { deep: true }),
component: `stage-${s.id}-component`,
}));
......@@ -3,10 +3,27 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import httpStatusCodes from '~/lib/utils/http_status';
import { selectedStage } from '../mock_data';
import { selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockStartDate = 30;
const mockRequestedDataActions = ['fetchValueStreams', 'fetchCycleAnalyticsData'];
const mockInitializeActionCommit = {
payload: { requestPath: mockRequestPath },
type: 'INITIALIZE_VSA',
};
const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
const mockRequestedDataMutations = [
{
payload: true,
type: 'SET_LOADING',
},
{
payload: false,
type: 'SET_LOADING',
},
];
describe('Project Value Stream Analytics actions', () => {
let state;
......@@ -22,27 +39,26 @@ describe('Project Value Stream Analytics actions', () => {
state = {};
});
it.each`
action | type | payload | expectedActions
${'initializeVsa'} | ${'INITIALIZE_VSA'} | ${{ requestPath: mockRequestPath }} | ${['fetchCycleAnalyticsData']}
${'setDateRange'} | ${'SET_DATE_RANGE'} | ${{ startDate: 30 }} | ${[]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${{ selectedStage }} | ${[]}
`(
'$action should dispatch $expectedActions and commit $type',
({ action, type, payload, expectedActions }) =>
const mutationTypes = (arr) => arr.map(({ type }) => type);
describe.each`
action | payload | expectedActions | expectedMutations
${'initializeVsa'} | ${{ requestPath: mockRequestPath }} | ${mockRequestedDataActions} | ${[mockInitializeActionCommit, ...mockRequestedDataMutations]}
${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockRequestedDataActions} | ${[mockSetDateActionCommit, ...mockRequestedDataMutations]}
${'setSelectedStage'} | ${{ selectedStage }} | ${['fetchStageData']} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${['fetchValueStreamStages']} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
testAction({
action: actions[action],
state,
payload,
expectedMutations: [
{
type,
payload,
},
],
expectedMutations,
expectedActions: expectedActions.map((a) => ({ type: a })),
}),
);
}));
});
describe('fetchCycleAnalyticsData', () => {
beforeEach(() => {
......@@ -60,7 +76,7 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'REQUEST_CYCLE_ANALYTICS_DATA' },
{ type: 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS' },
],
expectedActions: [{ type: 'setSelectedStage' }, { type: 'fetchStageData' }],
expectedActions: [],
}));
describe('with a failing request', () => {
......@@ -85,7 +101,7 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('fetchStageData', () => {
const mockStagePath = `${mockRequestPath}/events/${selectedStage.name}.json`;
const mockStagePath = `${mockRequestPath}/events/${selectedStage.name}`;
beforeEach(() => {
state = {
......@@ -153,4 +169,115 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
describe('fetchValueStreams', () => {
const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
beforeEach(() => {
state = {
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
});
it(`commits the 'REQUEST_VALUE_STREAMS' mutation`, () =>
testAction({
action: actions.fetchValueStreams,
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
}));
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAMS_ERROR' mutation`, () =>
testAction({
action: actions.fetchValueStreams,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_VALUE_STREAMS' },
{ type: 'RECEIVE_VALUE_STREAMS_ERROR', payload: httpStatusCodes.BAD_REQUEST },
],
expectedActions: [],
}));
});
});
describe('receiveValueStreamsSuccess', () => {
const mockValueStream = {
id: 'mockDefault',
name: 'mock default',
};
const mockValueStreams = [mockValueStream, selectedValueStream];
it('with data, will set the first value stream', () => {
testAction({
action: actions.receiveValueStreamsSuccess,
state,
payload: mockValueStreams,
expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: mockValueStreams }],
expectedActions: [{ type: 'setSelectedValueStream', payload: mockValueStream }],
});
});
it('without data, will set the default value stream', () => {
testAction({
action: actions.receiveValueStreamsSuccess,
state,
payload: [],
expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: [] }],
expectedActions: [{ type: 'setSelectedValueStream', payload: selectedValueStream }],
});
});
});
describe('fetchValueStreamStages', () => {
const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
beforeEach(() => {
state = {
fullPath: mockFullPath,
selectedValueStream,
};
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
});
it(`commits the 'REQUEST_VALUE_STREAM_STAGES' and 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' mutations`, () =>
testAction({
action: actions.fetchValueStreamStages,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_VALUE_STREAM_STAGES' },
{ type: 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' },
],
expectedActions: [],
}));
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
testAction({
action: actions.fetchValueStreamStages,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_VALUE_STREAM_STAGES' },
{ type: 'RECEIVE_VALUE_STREAM_STAGES_ERROR', payload: httpStatusCodes.BAD_REQUEST },
],
expectedActions: [],
}));
});
});
});
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import { selectedStage, rawEvents, convertedEvents, rawData, convertedData } from '../mock_data';
import {
selectedStage,
rawEvents,
convertedEvents,
rawData,
convertedData,
selectedValueStream,
rawValueStreamStages,
valueStreamStages,
} from '../mock_data';
let state;
const mockRequestPath = 'fake/request/path';
......@@ -17,15 +26,15 @@ describe('Project Value Stream Analytics mutations', () => {
it.each`
mutation | stateKey | value
${types.SET_SELECTED_STAGE} | ${'isLoadingStage'} | ${false}
${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]}
${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]}
${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]}
${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'stages'} | ${[]}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'hasError'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'isLoading'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'hasError'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'isLoading'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'hasError'} | ${true}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false}
${types.REQUEST_STAGE_DATA} | ${'hasError'} | ${false}
......@@ -44,12 +53,15 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
mutation | payload | stateKey | value
${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'stages'} | ${convertedData.stages}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
mutation | payload | stateKey | value
${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
......
......@@ -53,17 +53,6 @@ describe('Value stream analytics utils', () => {
expect(result.summary).toEqual(convertedData.summary);
});
it('returns the stages data', () => {
expect(result.stages).toEqual(convertedData.stages);
});
it('returns each of the default value stream stages', () => {
const stages = result.stages.map(({ name }) => name);
['issue', 'plan', 'code', 'test', 'review', 'staging'].forEach((stageName) => {
expect(stages).toContain(stageName);
});
});
it('returns `-` for summary data that has no value', () => {
const singleSummaryResult = decorateData({
stats: [],
......@@ -72,24 +61,6 @@ describe('Value stream analytics utils', () => {
});
expect(singleSummaryResult.summary).toEqual([{ value: '-', title: 'Commits' }]);
});
it('returns additional fields for each stage', () => {
const singleStageResult = decorateData({
stats: [{ name: 'issue', value: null }],
permissions: { issue: false },
});
const stage = singleStageResult.stages[0];
const txt =
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.';
expect(stage).toMatchObject({
active: false,
isUserAllowed: false,
emptyStageText: txt,
slug: 'issue',
component: 'stage-issue-component',
});
});
});
describe('transformStagesForPathNavigation', () => {
......
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