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 { joinPaths } from '~/lib/utils/url_utility';
import { buildApiUrl } from './api_utils';
const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics/value_streams';
......@@ -33,7 +34,7 @@ export const getProjectValueStreamStages = (requestPath, valueStreamId) => {
// 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 });
axios.get(joinPaths(requestPath, 'events', stageId), { params });
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
......@@ -46,7 +47,7 @@ export const getProjectValueStreamMetrics = (requestPath, params) =>
export const getValueStreamStageMedian = ({ requestPath, valueStreamId, stageId }, params = {}) => {
const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(`${stageBase}/median`, { params });
return axios.get(joinPaths(stageBase, 'median'), { params });
};
export const getValueStreamStageRecords = (
......@@ -54,5 +55,10 @@ export const getValueStreamStageRecords = (
params = {},
) => {
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 {
'summary',
'daysInPast',
'permissions',
'stageCounts',
]),
...mapGetters(['pathNavigationData']),
displayStageEvents() {
......@@ -77,6 +78,16 @@ export default {
? this.selectedStage?.emptyStageText
: '';
},
selectedStageCount() {
if (this.selectedStage) {
const {
stageCounts,
selectedStage: { id },
} = this;
return stageCounts[id];
}
return 0;
},
},
methods: {
...mapActions([
......@@ -117,7 +128,6 @@ export default {
:loading="isLoading || isLoadingStage"
:stages="pathNavigationData"
:selected-stage="selectedStage"
:with-stage-counts="false"
@selected="onSelectStage"
/>
<gl-loading-icon v-if="isLoading" size="lg" />
......@@ -162,7 +172,7 @@ export default {
:is-loading="isLoading || isLoadingStage"
:stage-events="selectedStageEvents"
:selected-stage="selectedStage"
:stage-count="null"
:stage-count="selectedStageCount"
:empty-state-title="emptyStageTitle"
:empty-state-message="emptyStageText"
:no-data-svg-path="noDataSvgPath"
......
......@@ -36,11 +36,6 @@ export default {
required: false,
default: () => ({}),
},
withStageCounts: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
showPopover({ id }) {
......@@ -81,7 +76,7 @@ export default {
<div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</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-pr-4 gl-pb-4">
{{ s__('ValueStreamEvent|Items in stage') }}
......
......@@ -4,6 +4,7 @@ import {
getProjectValueStreamMetrics,
getValueStreamStageMedian,
getValueStreamStageRecords,
getValueStreamStageCounts,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
......@@ -44,7 +45,7 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state;
commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage', 'fetchStageMedians'];
const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
......@@ -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) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
......
......@@ -24,3 +24,7 @@ export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
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 {
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
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 () => ({
selectedStageEvents: [],
selectedStageError: '',
medians: {},
stageCounts: {},
hasError: false,
isLoading: false,
isLoadingStage: false,
......
......@@ -33541,6 +33541,9 @@ msgstr ""
msgid "There was an error fetching projects"
msgstr ""
msgid "There was an error fetching stage total counts"
msgstr ""
msgid "There was an error fetching the %{replicableType}"
msgstr ""
......
......@@ -16,11 +16,13 @@ import {
createdBefore,
createdAfter,
currentGroup,
stageCounts,
} from './mock_data';
const selectedStageEvents = issueEvents.events;
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
const selectedStageCount = stageCounts[selectedStage.id];
Vue.use(Vuex);
......@@ -31,6 +33,7 @@ const defaultState = {
currentGroup,
createdBefore,
createdAfter,
stageCounts,
};
function createStore({ initialState = {}, initialGetters = {} }) {
......@@ -83,6 +86,10 @@ describe('Value stream analytics component', () => {
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', () => {
expect(findOverviewMetrics().exists()).toBe(true);
});
......@@ -91,6 +98,10 @@ describe('Value stream analytics component', () => {
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', () => {
expect(findStageEvents()).toEqual(selectedStageEvents);
});
......
......@@ -137,6 +137,24 @@ export const stagingEvents = deepCamelCase(stageFixtures.staging);
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 = [
{ id: 'issue', value: 172800 },
{ id: 'plan', value: 86400 },
......@@ -170,7 +188,7 @@ export const transformedProjectStagePathData = [
{
metric: 172800,
selected: true,
stageCount: undefined,
stageCount: 6,
icon: null,
id: 'issue',
title: 'Issue',
......@@ -182,7 +200,7 @@ export const transformedProjectStagePathData = [
{
metric: 86400,
selected: false,
stageCount: undefined,
stageCount: 6,
icon: null,
id: 'plan',
title: 'Plan',
......@@ -194,7 +212,7 @@ export const transformedProjectStagePathData = [
{
metric: 129600,
selected: false,
stageCount: undefined,
stageCount: 1,
icon: null,
id: 'code',
title: 'Code',
......
......@@ -216,6 +216,7 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'receiveValueStreamsSuccess' },
{ type: 'setSelectedStage' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
],
}));
......@@ -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 {
stageMedians,
transformedProjectStagePathData,
selectedStage,
stageCounts,
} from '../mock_data';
describe('Value stream analytics getters', () => {
describe('pathNavigationData', () => {
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);
});
});
......
......@@ -13,6 +13,8 @@ import {
valueStreamStages,
rawStageMedians,
formattedStageMedians,
rawStageCounts,
stageCounts,
} from '../mock_data';
let state;
......@@ -57,6 +59,8 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.REQUEST_STAGE_MEDIANS} | ${'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 }) => {
mutations[mutation](state);
......@@ -97,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${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, 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