Commit 6f804583 authored by Mark Florian's avatar Mark Florian

Merge branch 'ek-add-vsa-stage-counts-request' into 'master'

Adds stage counts to project VSA

See merge request gitlab-org/gitlab!67556
parents d5aba68e 3fba1465
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { buildApiUrl } from './api_utils'; import { buildApiUrl } from './api_utils';
const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics/value_streams'; const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics/value_streams';
...@@ -33,7 +34,7 @@ export const getProjectValueStreamStages = (requestPath, valueStreamId) => { ...@@ -33,7 +34,7 @@ export const getProjectValueStreamStages = (requestPath, valueStreamId) => {
// NOTE: legacy VSA request use a different path // NOTE: legacy VSA request use a different path
// the `requestPath` provides a full url for the request // the `requestPath` provides a full url for the request
export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) => export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) =>
axios.get(`${requestPath}/events/${stageId}`, { params }); axios.get(joinPaths(requestPath, 'events', stageId), { params });
export const getProjectValueStreamMetrics = (requestPath, params) => export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params }); axios.get(requestPath, { params });
...@@ -46,7 +47,7 @@ export const getProjectValueStreamMetrics = (requestPath, params) => ...@@ -46,7 +47,7 @@ export const getProjectValueStreamMetrics = (requestPath, params) =>
export const getValueStreamStageMedian = ({ requestPath, valueStreamId, stageId }, params = {}) => { export const getValueStreamStageMedian = ({ requestPath, valueStreamId, stageId }, params = {}) => {
const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId }); const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(`${stageBase}/median`, { params }); return axios.get(joinPaths(stageBase, 'median'), { params });
}; };
export const getValueStreamStageRecords = ( export const getValueStreamStageRecords = (
...@@ -54,5 +55,10 @@ export const getValueStreamStageRecords = ( ...@@ -54,5 +55,10 @@ export const getValueStreamStageRecords = (
params = {}, params = {},
) => { ) => {
const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId }); const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(`${stageBase}/records`, { params }); return axios.get(joinPaths(stageBase, 'records'), { params });
};
export const getValueStreamStageCounts = ({ requestPath, valueStreamId, stageId }, params = {}) => {
const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(joinPaths(stageBase, 'count'), { params });
}; };
...@@ -44,6 +44,7 @@ export default { ...@@ -44,6 +44,7 @@ export default {
'summary', 'summary',
'daysInPast', 'daysInPast',
'permissions', 'permissions',
'stageCounts',
]), ]),
...mapGetters(['pathNavigationData']), ...mapGetters(['pathNavigationData']),
displayStageEvents() { displayStageEvents() {
...@@ -77,6 +78,16 @@ export default { ...@@ -77,6 +78,16 @@ export default {
? this.selectedStage?.emptyStageText ? this.selectedStage?.emptyStageText
: ''; : '';
}, },
selectedStageCount() {
if (this.selectedStage) {
const {
stageCounts,
selectedStage: { id },
} = this;
return stageCounts[id];
}
return 0;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -117,7 +128,6 @@ export default { ...@@ -117,7 +128,6 @@ export default {
:loading="isLoading || isLoadingStage" :loading="isLoading || isLoadingStage"
:stages="pathNavigationData" :stages="pathNavigationData"
:selected-stage="selectedStage" :selected-stage="selectedStage"
:with-stage-counts="false"
@selected="onSelectStage" @selected="onSelectStage"
/> />
<gl-loading-icon v-if="isLoading" size="lg" /> <gl-loading-icon v-if="isLoading" size="lg" />
...@@ -162,7 +172,7 @@ export default { ...@@ -162,7 +172,7 @@ export default {
:is-loading="isLoading || isLoadingStage" :is-loading="isLoading || isLoadingStage"
:stage-events="selectedStageEvents" :stage-events="selectedStageEvents"
:selected-stage="selectedStage" :selected-stage="selectedStage"
:stage-count="null" :stage-count="selectedStageCount"
:empty-state-title="emptyStageTitle" :empty-state-title="emptyStageTitle"
:empty-state-message="emptyStageText" :empty-state-message="emptyStageText"
:no-data-svg-path="noDataSvgPath" :no-data-svg-path="noDataSvgPath"
......
...@@ -36,11 +36,6 @@ export default { ...@@ -36,11 +36,6 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
withStageCounts: {
type: Boolean,
required: false,
default: true,
},
}, },
methods: { methods: {
showPopover({ id }) { showPopover({ id }) {
...@@ -81,7 +76,7 @@ export default { ...@@ -81,7 +76,7 @@ export default {
<div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</div> <div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</div>
</div> </div>
</div> </div>
<div v-if="withStageCounts" class="gl-px-4"> <div class="gl-px-4">
<div class="gl-display-flex gl-justify-content-space-between"> <div class="gl-display-flex gl-justify-content-space-between">
<div class="gl-pr-4 gl-pb-4"> <div class="gl-pr-4 gl-pb-4">
{{ s__('ValueStreamEvent|Items in stage') }} {{ s__('ValueStreamEvent|Items in stage') }}
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
getProjectValueStreamMetrics, getProjectValueStreamMetrics,
getValueStreamStageMedian, getValueStreamStageMedian,
getValueStreamStageRecords, getValueStreamStageRecords,
getValueStreamStageCounts,
} from '~/api/analytics_api'; } from '~/api/analytics_api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -44,7 +45,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => { ...@@ -44,7 +45,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state; } = state;
commit(types.REQUEST_VALUE_STREAMS); commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage', 'fetchStageMedians']; const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath) return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data)) .then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r)))) .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
...@@ -115,6 +116,37 @@ export const fetchStageMedians = ({ ...@@ -115,6 +116,37 @@ export const fetchStageMedians = ({
}); });
}; };
const getStageCounts = ({ stageId, vsaParams, filterParams = {} }) => {
return getValueStreamStageCounts({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
id: stageId,
...data,
}));
};
export const fetchStageCountValues = ({
state: { stages },
getters: { requestParams: vsaParams, filterParams },
commit,
}) => {
commit(types.REQUEST_STAGE_COUNTS);
return Promise.all(
stages.map(({ id: stageId }) =>
getStageCounts({
vsaParams,
stageId,
filterParams,
}),
),
)
.then((data) => commit(types.RECEIVE_STAGE_COUNTS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_COUNTS_ERROR, error);
createFlash({
message: __('There was an error fetching stage total counts'),
});
});
};
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => { export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0]; const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage); commit(types.SET_SELECTED_STAGE, stage);
......
...@@ -24,3 +24,7 @@ export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR'; ...@@ -24,3 +24,7 @@ export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS'; export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS'; export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR'; export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
export const REQUEST_STAGE_COUNTS = 'REQUEST_STAGE_COUNTS';
export const RECEIVE_STAGE_COUNTS_SUCCESS = 'RECEIVE_STAGE_COUNTS_SUCCESS';
export const RECEIVE_STAGE_COUNTS_ERROR = 'RECEIVE_STAGE_COUNTS_ERROR';
...@@ -87,4 +87,19 @@ export default { ...@@ -87,4 +87,19 @@ export default {
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) { [types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {}; state.medians = {};
}, },
[types.REQUEST_STAGE_COUNTS](state) {
state.stageCounts = {};
},
[types.RECEIVE_STAGE_COUNTS_SUCCESS](state, stageCounts = []) {
state.stageCounts = stageCounts.reduce(
(acc, { id, count }) => ({
...acc,
[id]: count,
}),
{},
);
},
[types.RECEIVE_STAGE_COUNTS_ERROR](state) {
state.stageCounts = {};
},
}; };
...@@ -16,6 +16,7 @@ export default () => ({ ...@@ -16,6 +16,7 @@ export default () => ({
selectedStageEvents: [], selectedStageEvents: [],
selectedStageError: '', selectedStageError: '',
medians: {}, medians: {},
stageCounts: {},
hasError: false, hasError: false,
isLoading: false, isLoading: false,
isLoadingStage: false, isLoadingStage: false,
......
...@@ -33541,6 +33541,9 @@ msgstr "" ...@@ -33541,6 +33541,9 @@ msgstr ""
msgid "There was an error fetching projects" msgid "There was an error fetching projects"
msgstr "" msgstr ""
msgid "There was an error fetching stage total counts"
msgstr ""
msgid "There was an error fetching the %{replicableType}" msgid "There was an error fetching the %{replicableType}"
msgstr "" msgstr ""
......
...@@ -16,11 +16,13 @@ import { ...@@ -16,11 +16,13 @@ import {
createdBefore, createdBefore,
createdAfter, createdAfter,
currentGroup, currentGroup,
stageCounts,
} from './mock_data'; } from './mock_data';
const selectedStageEvents = issueEvents.events; const selectedStageEvents = issueEvents.events;
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access'; const noAccessSvgPath = 'path/to/no/access';
const selectedStageCount = stageCounts[selectedStage.id];
Vue.use(Vuex); Vue.use(Vuex);
...@@ -31,6 +33,7 @@ const defaultState = { ...@@ -31,6 +33,7 @@ const defaultState = {
currentGroup, currentGroup,
createdBefore, createdBefore,
createdAfter, createdAfter,
stageCounts,
}; };
function createStore({ initialState = {}, initialGetters = {} }) { function createStore({ initialState = {}, initialGetters = {} }) {
...@@ -83,6 +86,10 @@ describe('Value stream analytics component', () => { ...@@ -83,6 +86,10 @@ describe('Value stream analytics component', () => {
expect(findPathNavigation().exists()).toBe(true); expect(findPathNavigation().exists()).toBe(true);
}); });
it('receives the stages formatted for the path navigation', () => {
expect(findPathNavigation().props('stages')).toBe(transformedProjectStagePathData);
});
it('renders the overview metrics', () => { it('renders the overview metrics', () => {
expect(findOverviewMetrics().exists()).toBe(true); expect(findOverviewMetrics().exists()).toBe(true);
}); });
...@@ -91,6 +98,10 @@ describe('Value stream analytics component', () => { ...@@ -91,6 +98,10 @@ describe('Value stream analytics component', () => {
expect(findStageTable().exists()).toBe(true); expect(findStageTable().exists()).toBe(true);
}); });
it('passes the selected stage count to the stage table', () => {
expect(findStageTable().props('stageCount')).toBe(selectedStageCount);
});
it('renders the stage table events', () => { it('renders the stage table events', () => {
expect(findStageEvents()).toEqual(selectedStageEvents); expect(findStageEvents()).toEqual(selectedStageEvents);
}); });
......
...@@ -137,6 +137,24 @@ export const stagingEvents = deepCamelCase(stageFixtures.staging); ...@@ -137,6 +137,24 @@ export const stagingEvents = deepCamelCase(stageFixtures.staging);
export const pathNavIssueMetric = 172800; export const pathNavIssueMetric = 172800;
export const rawStageCounts = [
{ id: 'issue', count: 6 },
{ id: 'plan', count: 6 },
{ id: 'code', count: 1 },
{ id: 'test', count: 5 },
{ id: 'review', count: 12 },
{ id: 'staging', count: 3 },
];
export const stageCounts = {
code: 1,
issue: 6,
plan: 6,
review: 12,
staging: 3,
test: 5,
};
export const rawStageMedians = [ export const rawStageMedians = [
{ id: 'issue', value: 172800 }, { id: 'issue', value: 172800 },
{ id: 'plan', value: 86400 }, { id: 'plan', value: 86400 },
...@@ -170,7 +188,7 @@ export const transformedProjectStagePathData = [ ...@@ -170,7 +188,7 @@ export const transformedProjectStagePathData = [
{ {
metric: 172800, metric: 172800,
selected: true, selected: true,
stageCount: undefined, stageCount: 6,
icon: null, icon: null,
id: 'issue', id: 'issue',
title: 'Issue', title: 'Issue',
...@@ -182,7 +200,7 @@ export const transformedProjectStagePathData = [ ...@@ -182,7 +200,7 @@ export const transformedProjectStagePathData = [
{ {
metric: 86400, metric: 86400,
selected: false, selected: false,
stageCount: undefined, stageCount: 6,
icon: null, icon: null,
id: 'plan', id: 'plan',
title: 'Plan', title: 'Plan',
...@@ -194,7 +212,7 @@ export const transformedProjectStagePathData = [ ...@@ -194,7 +212,7 @@ export const transformedProjectStagePathData = [
{ {
metric: 129600, metric: 129600,
selected: false, selected: false,
stageCount: undefined, stageCount: 1,
icon: null, icon: null,
id: 'code', id: 'code',
title: 'Code', title: 'Code',
......
...@@ -216,6 +216,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -216,6 +216,7 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'receiveValueStreamsSuccess' }, { type: 'receiveValueStreamsSuccess' },
{ type: 'setSelectedStage' }, { type: 'setSelectedStage' },
{ type: 'fetchStageMedians' }, { type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
], ],
})); }));
...@@ -364,4 +365,64 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -364,4 +365,64 @@ describe('Project Value Stream Analytics actions', () => {
})); }));
}); });
}); });
describe('fetchStageCountValues', () => {
const mockValueStreamPath = /count/;
const stageCountsPayload = [
{ id: 'issue', count: 1 },
{ id: 'plan', count: 2 },
{ id: 'code', count: 3 },
];
const stageCountError = new Error(
`Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
);
beforeEach(() => {
state = {
fullPath: mockFullPath,
selectedValueStream,
stages: allowedStages,
};
mock = new MockAdapter(axios);
mock
.onGet(mockValueStreamPath)
.replyOnce(httpStatusCodes.OK, { count: 1 })
.onGet(mockValueStreamPath)
.replyOnce(httpStatusCodes.OK, { count: 2 })
.onGet(mockValueStreamPath)
.replyOnce(httpStatusCodes.OK, { count: 3 });
});
it(`commits the 'REQUEST_STAGE_COUNTS' and 'RECEIVE_STAGE_COUNTS_SUCCESS' mutations`, () =>
testAction({
action: actions.fetchStageCountValues,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_STAGE_COUNTS' },
{ type: 'RECEIVE_STAGE_COUNTS_SUCCESS', payload: stageCountsPayload },
],
expectedActions: [],
}));
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
});
it(`commits the 'RECEIVE_STAGE_COUNTS_ERROR' mutation`, () =>
testAction({
action: actions.fetchStageCountValues,
state,
payload: {},
expectedMutations: [
{ type: 'REQUEST_STAGE_COUNTS' },
{ type: 'RECEIVE_STAGE_COUNTS_ERROR', payload: stageCountError },
],
expectedActions: [],
}));
});
});
}); });
...@@ -4,12 +4,13 @@ import { ...@@ -4,12 +4,13 @@ import {
stageMedians, stageMedians,
transformedProjectStagePathData, transformedProjectStagePathData,
selectedStage, selectedStage,
stageCounts,
} from '../mock_data'; } from '../mock_data';
describe('Value stream analytics getters', () => { describe('Value stream analytics getters', () => {
describe('pathNavigationData', () => { describe('pathNavigationData', () => {
it('returns the transformed data', () => { it('returns the transformed data', () => {
const state = { stages: allowedStages, medians: stageMedians, selectedStage }; const state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts };
expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData); expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData);
}); });
}); });
......
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
valueStreamStages, valueStreamStages,
rawStageMedians, rawStageMedians,
formattedStageMedians, formattedStageMedians,
rawStageCounts,
stageCounts,
} from '../mock_data'; } from '../mock_data';
let state; let state;
...@@ -57,6 +59,8 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -57,6 +59,8 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}}
${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => { `('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state); mutations[mutation](state);
...@@ -97,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -97,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]} ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages} ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians} ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
`( `(
'$mutation with $payload will set $stateKey to $value', '$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => { ({ mutation, payload, stateKey, value }) => {
......
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