Commit 0379cc17 authored by Mark Florian's avatar Mark Florian

Merge branch '335953-add-dedicated-project-vsa-endpoints' into 'master'

Add dedicated endpoints for project VSA

See merge request gitlab-org/gitlab!67204
parents 10227591 552bcfa3
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils'; import { buildApiUrl } from './api_utils';
const GROUP_VSA_PATH_BASE = const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics/value_streams';
'/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
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 PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
const PROJECT_VSA_STAGE_DATA_PATH = `${PROJECT_VSA_STAGES_PATH}/:stage_id`;
const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => { const buildProjectValueStreamPath = (requestPath, valueStreamId = null) => {
if (valueStreamId) { if (valueStreamId) {
return buildApiUrl(PROJECT_VSA_STAGES_PATH) return buildApiUrl(PROJECT_VSA_STAGES_PATH)
.replace(':project_path', projectPath) .replace(':request_path', requestPath)
.replace(':value_stream_id', valueStreamId); .replace(':value_stream_id', valueStreamId);
} }
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath); return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':request_path', requestPath);
}; };
const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) => const buildValueStreamStageDataPath = ({ requestPath, valueStreamId = null, stageId = null }) =>
buildApiUrl(GROUP_VSA_PATH_BASE) buildApiUrl(PROJECT_VSA_STAGE_DATA_PATH)
.replace(':id', groupId) .replace(':request_path', requestPath)
.replace(':value_stream_id', valueStreamId) .replace(':value_stream_id', valueStreamId)
.replace(':stage_id', stageId); .replace(':stage_id', stageId);
export const getProjectValueStreams = (projectPath) => { export const getProjectValueStreams = (requestPath) => {
const url = buildProjectValueStreamPath(projectPath); const url = buildProjectValueStreamPath(requestPath);
return axios.get(url); return axios.get(url);
}; };
export const getProjectValueStreamStages = (projectPath, valueStreamId) => { export const getProjectValueStreamStages = (requestPath, valueStreamId) => {
const url = buildProjectValueStreamPath(projectPath, valueStreamId); const url = buildProjectValueStreamPath(requestPath, valueStreamId);
return axios.get(url); return axios.get(url);
}; };
...@@ -45,7 +44,15 @@ export const getProjectValueStreamMetrics = (requestPath, params) => ...@@ -45,7 +44,15 @@ export const getProjectValueStreamMetrics = (requestPath, params) =>
* When used for project level VSA, requests should include the `project_id` in the params object * When used for project level VSA, requests should include the `project_id` in the params object
*/ */
export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => { export const getValueStreamStageMedian = ({ requestPath, valueStreamId, stageId }, params = {}) => {
const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId }); const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(`${stageBase}/median`, { params }); return axios.get(`${stageBase}/median`, { params });
}; };
export const getValueStreamStageRecords = (
{ requestPath, valueStreamId, stageId },
params = {},
) => {
const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
return axios.get(`${stageBase}/records`, { params });
};
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
'selectedStageError', 'selectedStageError',
'stages', 'stages',
'summary', 'summary',
'startDate', 'daysInPast',
'permissions', 'permissions',
]), ]),
...mapGetters(['pathNavigationData']), ...mapGetters(['pathNavigationData']),
...@@ -51,13 +51,15 @@ export default { ...@@ -51,13 +51,15 @@ export default {
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage; return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
}, },
displayNotEnoughData() { displayNotEnoughData() {
return this.selectedStageReady && this.isEmptyStage; return !this.isLoadingStage && this.isEmptyStage;
}, },
displayNoAccess() { displayNoAccess() {
return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id); return (
!this.isLoadingStage && this.selectedStage?.id && !this.isUserAllowed(this.selectedStage.id)
);
}, },
selectedStageReady() { displayPathNavigation() {
return !this.isLoadingStage && this.selectedStage; return this.isLoading || (this.selectedStage && this.pathNavigationData.length);
}, },
emptyStageTitle() { emptyStageTitle() {
if (this.displayNoAccess) { if (this.displayNoAccess) {
...@@ -83,8 +85,8 @@ export default { ...@@ -83,8 +85,8 @@ export default {
'setSelectedStage', 'setSelectedStage',
'setDateRange', 'setDateRange',
]), ]),
handleDateSelect(startDate) { handleDateSelect(daysInPast) {
this.setDateRange({ startDate }); this.setDateRange(daysInPast);
}, },
onSelectStage(stage) { onSelectStage(stage) {
this.setSelectedStage(stage); this.setSelectedStage(stage);
...@@ -101,15 +103,18 @@ export default { ...@@ -101,15 +103,18 @@ export default {
dayRangeOptions: [7, 30, 90], dayRangeOptions: [7, 30, 90],
i18n: { i18n: {
dropdownText: __('Last %{days} days'), dropdownText: __('Last %{days} days'),
pageTitle: __('Value Stream Analytics'),
recentActivity: __('Recent Project Activity'),
}, },
}; };
</script> </script>
<template> <template>
<div class="cycle-analytics"> <div class="cycle-analytics">
<h3>{{ $options.i18n.pageTitle }}</h3>
<path-navigation <path-navigation
v-if="selectedStageReady" v-if="displayPathNavigation"
class="js-path-navigation gl-w-full gl-pb-2" class="js-path-navigation gl-w-full gl-pb-2"
:loading="isLoading" :loading="isLoading || isLoadingStage"
:stages="pathNavigationData" :stages="pathNavigationData"
:selected-stage="selectedStage" :selected-stage="selectedStage"
:with-stage-counts="false" :with-stage-counts="false"
...@@ -135,7 +140,7 @@ export default { ...@@ -135,7 +140,7 @@ export default {
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
<span class="dropdown-label"> <span class="dropdown-label">
<gl-sprintf :message="$options.i18n.dropdownText"> <gl-sprintf :message="$options.i18n.dropdownText">
<template #days>{{ startDate }}</template> <template #days>{{ daysInPast }}</template>
</gl-sprintf> </gl-sprintf>
<gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" /> <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
</span> </span>
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
selectedStage: { selectedStage: {
type: Object, type: Object,
required: false, required: false,
default: () => ({ custom: false }), default: () => ({}),
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -102,7 +102,7 @@ export default { ...@@ -102,7 +102,7 @@ export default {
}, },
computed: { computed: {
isEmptyStage() { isEmptyStage() {
return !this.stageEvents.length; return !this.selectedStage || !this.stageEvents.length;
}, },
emptyStateTitleText() { emptyStateTitleText() {
return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR; return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
......
...@@ -20,11 +20,9 @@ export default () => { ...@@ -20,11 +20,9 @@ export default () => {
store.dispatch('initializeVsa', { store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10), projectId: parseInt(projectId, 10),
groupPath, groupPath,
endpoints: {
requestPath, requestPath,
fullPath, fullPath,
features: {
cycleAnalyticsForGroups:
(groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
}, },
}); });
......
import { import {
getProjectValueStreamStages, getProjectValueStreamStages,
getProjectValueStreams, getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics, getProjectValueStreamMetrics,
getValueStreamStageMedian, getValueStreamStageMedian,
getValueStreamStageRecords,
} from '~/api/analytics_api'; } from '~/api/analytics_api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
DEFAULT_DAYS_TO_DISPLAY,
DEFAULT_VALUE_STREAM,
I18N_VSA_ERROR_STAGE_MEDIAN,
} from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream); commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
return dispatch('fetchValueStreamStages'); return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
}; };
export const fetchValueStreamStages = ({ commit, state }) => { export const fetchValueStreamStages = ({ commit, state }) => {
const { fullPath, selectedValueStream } = state; const {
endpoints: { fullPath },
selectedValueStream: { id },
} = state;
commit(types.REQUEST_VALUE_STREAM_STAGES); commit(types.REQUEST_VALUE_STREAM_STAGES);
return getProjectValueStreamStages(fullPath, selectedValueStream.id) return getProjectValueStreamStages(fullPath, id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data)) .then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
.catch(({ response: { status } }) => { .catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAM_STAGES_ERROR, status); commit(types.RECEIVE_VALUE_STREAM_STAGES_ERROR, status);
...@@ -41,16 +40,11 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { ...@@ -41,16 +40,11 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
export const fetchValueStreams = ({ commit, dispatch, state }) => { export const fetchValueStreams = ({ commit, dispatch, state }) => {
const { const {
fullPath, endpoints: { fullPath },
features: { cycleAnalyticsForGroups },
} = state; } = state;
commit(types.REQUEST_VALUE_STREAMS); commit(types.REQUEST_VALUE_STREAMS);
const stageRequests = ['setSelectedStage']; const stageRequests = ['setSelectedStage', 'fetchStageMedians'];
if (cycleAnalyticsForGroups) {
stageRequests.push('fetchStageMedians');
}
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))))
...@@ -58,9 +52,10 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => { ...@@ -58,9 +52,10 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status); commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
}); });
}; };
export const fetchCycleAnalyticsData = ({ export const fetchCycleAnalyticsData = ({
state: { requestPath }, state: {
endpoints: { requestPath },
},
getters: { legacyFilterParams }, getters: { legacyFilterParams },
commit, commit,
}) => { }) => {
...@@ -76,18 +71,10 @@ export const fetchCycleAnalyticsData = ({ ...@@ -76,18 +71,10 @@ export const fetchCycleAnalyticsData = ({
}); });
}; };
export const fetchStageData = ({ export const fetchStageData = ({ getters: { requestParams, filterParams }, commit }) => {
state: { requestPath, selectedStage },
getters: { legacyFilterParams },
commit,
}) => {
commit(types.REQUEST_STAGE_DATA); commit(types.REQUEST_STAGE_DATA);
return getProjectValueStreamStageData({ return getValueStreamStageRecords(requestParams, filterParams)
requestPath,
stageId: selectedStage.id,
params: legacyFilterParams,
})
.then(({ data }) => { .then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data // when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) { if (data?.error) {
...@@ -134,22 +121,32 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select ...@@ -134,22 +121,32 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select
return dispatch('fetchStageData'); return dispatch('fetchStageData');
}; };
const refetchData = (dispatch, commit) => { export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
commit(types.SET_LOADING, true);
const refetchStageData = (dispatch) => {
return Promise.resolve() return Promise.resolve()
.then(() => dispatch('fetchValueStreams')) .then(() => dispatch('setLoading', true))
.then(() => dispatch('fetchCycleAnalyticsData')) .then(() =>
.finally(() => commit(types.SET_LOADING, false)); Promise.all([
dispatch('fetchCycleAnalyticsData'),
dispatch('fetchStageData'),
dispatch('fetchStageMedians'),
]),
)
.finally(() => dispatch('setLoading', false));
}; };
export const setFilters = ({ dispatch, commit }) => refetchData(dispatch, commit); export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => { export const setDateRange = ({ dispatch, commit }, daysInPast) => {
commit(types.SET_DATE_RANGE, { startDate }); commit(types.SET_DATE_RANGE, daysInPast);
return refetchData(dispatch, commit); return refetchStageData(dispatch);
}; };
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
return refetchData(dispatch, commit);
return dispatch('setLoading', true)
.then(() => dispatch('fetchValueStreams'))
.finally(() => dispatch('setLoading', false));
}; };
...@@ -13,11 +13,11 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage ...@@ -13,11 +13,11 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
export const requestParams = (state) => { export const requestParams = (state) => {
const { const {
selectedStage: { id: stageId = null }, endpoints: { fullPath },
groupPath: groupId,
selectedValueStream: { id: valueStreamId }, selectedValueStream: { id: valueStreamId },
selectedStage: { id: stageId = null },
} = state; } = state;
return { valueStreamId, groupId, stageId }; return { requestPath: fullPath, valueStreamId, stageId };
}; };
const dateRangeParams = ({ createdAfter, createdBefore }) => ({ const dateRangeParams = ({ createdAfter, createdBefore }) => ({
...@@ -25,15 +25,14 @@ const dateRangeParams = ({ createdAfter, createdBefore }) => ({ ...@@ -25,15 +25,14 @@ const dateRangeParams = ({ createdAfter, createdBefore }) => ({
created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null, created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
}); });
export const legacyFilterParams = ({ startDate }) => { export const legacyFilterParams = ({ daysInPast }) => {
return { return {
'cycle_analytics[start_date]': startDate, 'cycle_analytics[start_date]': daysInPast,
}; };
}; };
export const filterParams = ({ id, ...rest }) => { export const filterParams = (state) => {
return { return {
project_ids: [id], ...dateRangeParams(state),
...dateRangeParams(rest),
}; };
}; };
...@@ -4,15 +4,11 @@ import { decorateData, formatMedianValues, calculateFormattedDayInPast } from '. ...@@ -4,15 +4,11 @@ import { decorateData, formatMedianValues, calculateFormattedDayInPast } from '.
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) { [types.INITIALIZE_VSA](state, { endpoints }) {
state.requestPath = requestPath; state.endpoints = endpoints;
state.fullPath = fullPath;
state.groupPath = groupPath;
state.id = projectId;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
state.createdBefore = now; state.createdBefore = now;
state.createdAfter = past; state.createdAfter = past;
state.features = features;
}, },
[types.SET_LOADING](state, loadingState) { [types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState; state.isLoading = loadingState;
...@@ -23,9 +19,9 @@ export default { ...@@ -23,9 +19,9 @@ export default {
[types.SET_SELECTED_STAGE](state, stage) { [types.SET_SELECTED_STAGE](state, stage) {
state.selectedStage = stage; state.selectedStage = stage;
}, },
[types.SET_DATE_RANGE](state, { startDate }) { [types.SET_DATE_RANGE](state, daysInPast) {
state.startDate = startDate; state.daysInPast = daysInPast;
const { now, past } = calculateFormattedDayInPast(startDate); const { now, past } = calculateFormattedDayInPast(daysInPast);
state.createdBefore = now; state.createdBefore = now;
state.createdAfter = past; state.createdAfter = past;
}, },
...@@ -50,25 +46,16 @@ export default { ...@@ -50,25 +46,16 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) { [types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true; state.isLoading = true;
state.hasError = false; state.hasError = false;
if (!state.features.cycleAnalyticsForGroups) {
state.medians = {};
}
}, },
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) { [types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
const { summary, medians } = decorateData(data); const { summary } = decorateData(data);
if (!state.features.cycleAnalyticsForGroups) { state.permissions = data?.permissions || {};
state.medians = formatMedianValues(medians);
}
state.permissions = data.permissions;
state.summary = summary; state.summary = summary;
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) { [types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false; state.isLoading = false;
state.hasError = true; state.hasError = true;
if (!state.features.cycleAnalyticsForGroups) {
state.medians = {};
}
}, },
[types.REQUEST_STAGE_DATA](state) { [types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true; state.isLoadingStage = true;
...@@ -76,7 +63,7 @@ export default { ...@@ -76,7 +63,7 @@ export default {
state.selectedStageEvents = []; state.selectedStageEvents = [];
state.hasError = false; state.hasError = false;
}, },
[types.RECEIVE_STAGE_DATA_SUCCESS](state, { events = [] }) { [types.RECEIVE_STAGE_DATA_SUCCESS](state, events = []) {
state.isLoadingStage = false; state.isLoadingStage = false;
state.isEmptyStage = !events.length; state.isEmptyStage = !events.length;
state.selectedStageEvents = events.map((ev) => state.selectedStageEvents = events.map((ev) =>
......
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({ export default () => ({
features: {},
id: null, id: null,
requestPath: '', endpoints: {},
fullPath: '', daysInPast: DEFAULT_DAYS_TO_DISPLAY,
startDate: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null, createdAfter: null,
createdBefore: null, createdBefore: null,
stages: [], stages: [],
...@@ -23,5 +21,4 @@ export default () => ({ ...@@ -23,5 +21,4 @@ export default () => ({
isLoadingStage: false, isLoadingStage: false,
isEmptyStage: false, isEmptyStage: false,
permissions: {}, permissions: {},
parentPath: null,
}); });
...@@ -8,13 +8,11 @@ import { parseSeconds } from '~/lib/utils/datetime_utility'; ...@@ -8,13 +8,11 @@ import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' }); const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
const mapToMedians = ({ name: id, value }) => ({ id, value });
export const decorateData = (data = {}) => { export const decorateData = (data = {}) => {
const { stats: stages, summary } = data; const { summary } = data;
return { return {
summary: summary?.map((item) => mapToSummary(item)) || [], summary: summary?.map((item) => mapToSummary(item)) || [],
medians: stages?.map((item) => mapToMedians(item)) || [],
}; };
}; };
......
import Api from 'ee/api'; import Api from 'ee/api';
import { getValueStreamStageMedian } from '~/api/analytics_api'; import { getGroupValueStreamStageMedian } from 'ee/api/analytics_api';
import { import {
I18N_VSA_ERROR_STAGES, I18N_VSA_ERROR_STAGES,
I18N_VSA_ERROR_STAGE_MEDIAN, I18N_VSA_ERROR_STAGE_MEDIAN,
...@@ -59,7 +59,7 @@ export const receiveStageMedianValuesError = ({ commit }, error) => { ...@@ -59,7 +59,7 @@ export const receiveStageMedianValuesError = ({ commit }, error) => {
}; };
const fetchStageMedian = ({ groupId, valueStreamId, stageId, params }) => const fetchStageMedian = ({ groupId, valueStreamId, stageId, params }) =>
getValueStreamStageMedian({ groupId, valueStreamId, stageId }, params).then(({ data }) => { getGroupValueStreamStageMedian({ groupId, valueStreamId, stageId }, params).then(({ data }) => {
return { return {
id: stageId, id: stageId,
...(data?.error ...(data?.error
......
import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const GROUP_VSA_PATH_BASE =
'/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) =>
buildApiUrl(GROUP_VSA_PATH_BASE)
.replace(':id', groupId)
.replace(':value_stream_id', valueStreamId)
.replace(':stage_id', stageId);
export const getGroupValueStreamStageMedian = (
{ groupId, valueStreamId, stageId },
params = {},
) => {
const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId });
return axios.get(`${stageBase}/median`, { params });
};
...@@ -46,9 +46,9 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -46,9 +46,9 @@ RSpec.describe 'Value Stream Analytics', :js do
@build = create_cycle(user, project, issue, mr, milestone, pipeline) @build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project) deploy_master(user, project)
issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.day) issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour)
merge_request = issue.merge_requests_closing_issues.first.merge_request merge_request = issue.merge_requests_closing_issues.first.merge_request
merge_request.update!(created_at: issue.metrics.first_associated_with_milestone_at + 1.day) merge_request.update!(created_at: issue.metrics.first_associated_with_milestone_at + 1.hour)
merge_request.metrics.update!( merge_request.metrics.update!(
latest_build_started_at: 4.hours.ago, latest_build_started_at: 4.hours.ago,
latest_build_finished_at: 3.hours.ago, latest_build_finished_at: 3.hours.ago,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`;
...@@ -8,7 +8,15 @@ import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; ...@@ -8,7 +8,15 @@ import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state'; import initState from '~/cycle_analytics/store/state';
import { selectedStage, issueEvents } from './mock_data'; import {
permissions,
transformedProjectStagePathData,
selectedStage,
issueEvents,
createdBefore,
createdAfter,
currentGroup,
} from './mock_data';
const selectedStageEvents = issueEvents.events; const selectedStageEvents = issueEvents.events;
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
...@@ -18,25 +26,31 @@ Vue.use(Vuex); ...@@ -18,25 +26,31 @@ Vue.use(Vuex);
let wrapper; let wrapper;
function createStore({ initialState = {} }) { const defaultState = {
permissions,
currentGroup,
createdBefore,
createdAfter,
};
function createStore({ initialState = {}, initialGetters = {} }) {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
...initState(), ...initState(),
permissions: { ...defaultState,
[selectedStage.id]: true,
},
...initialState, ...initialState,
}, },
getters: { getters: {
pathNavigationData: () => [], pathNavigationData: () => transformedProjectStagePathData,
...initialGetters,
}, },
}); });
} }
function createComponent({ initialState } = {}) { function createComponent({ initialState, initialGetters } = {}) {
return extendedWrapper( return extendedWrapper(
shallowMount(BaseComponent, { shallowMount(BaseComponent, {
store: createStore({ initialState }), store: createStore({ initialState, initialGetters }),
propsData: { propsData: {
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
...@@ -57,16 +71,7 @@ const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('tit ...@@ -57,16 +71,7 @@ const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('tit
describe('Value stream analytics component', () => { describe('Value stream analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents } });
initialState: {
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
selectedStageEvents,
selectedStage,
selectedStageError: '',
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -102,7 +107,7 @@ describe('Value stream analytics component', () => { ...@@ -102,7 +107,7 @@ describe('Value stream analytics component', () => {
}); });
it('renders the path navigation component with prop `loading` set to true', () => { it('renders the path navigation component with prop `loading` set to true', () => {
expect(findPathNavigation().html()).toMatchSnapshot(); expect(findPathNavigation().props('loading')).toBe(true);
}); });
it('does not render the overview metrics', () => { it('does not render the overview metrics', () => {
...@@ -130,13 +135,19 @@ describe('Value stream analytics component', () => { ...@@ -130,13 +135,19 @@ describe('Value stream analytics component', () => {
expect(tableWrapper.exists()).toBe(true); expect(tableWrapper.exists()).toBe(true);
expect(tableWrapper.find(GlLoadingIcon).exists()).toBe(true); expect(tableWrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
it('renders the path navigation loading state', () => {
expect(findPathNavigation().props('loading')).toBe(true);
});
}); });
describe('isEmptyStage = true', () => { describe('isEmptyStage = true', () => {
const emptyStageParams = {
isEmptyStage: true,
selectedStage: { ...selectedStage, emptyStageText: 'This stage is empty' },
};
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({ initialState: emptyStageParams });
initialState: { selectedStage, isEmptyStage: true },
});
}); });
it('renders the empty stage with `Not enough data` message', () => { it('renders the empty stage with `Not enough data` message', () => {
...@@ -147,8 +158,7 @@ describe('Value stream analytics component', () => { ...@@ -147,8 +158,7 @@ describe('Value stream analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialState: { initialState: {
selectedStage, ...emptyStageParams,
isEmptyStage: true,
selectedStageError: 'There is too much data to calculate', selectedStageError: 'There is too much data to calculate',
}, },
}); });
...@@ -164,7 +174,9 @@ describe('Value stream analytics component', () => { ...@@ -164,7 +174,9 @@ describe('Value stream analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialState: { initialState: {
selectedStage,
permissions: { permissions: {
...permissions,
[selectedStage.id]: false, [selectedStage.id]: false,
}, },
}, },
...@@ -179,6 +191,7 @@ describe('Value stream analytics component', () => { ...@@ -179,6 +191,7 @@ describe('Value stream analytics component', () => {
describe('without a selected stage', () => { describe('without a selected stage', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialGetters: { pathNavigationData: () => [] },
initialState: { selectedStage: null, isEmptyStage: true }, initialState: { selectedStage: null, isEmptyStage: true },
}); });
}); });
...@@ -187,7 +200,7 @@ describe('Value stream analytics component', () => { ...@@ -187,7 +200,7 @@ describe('Value stream analytics component', () => {
expect(findStageTable().exists()).toBe(true); expect(findStageTable().exists()).toBe(true);
}); });
it('does not render the path navigation component', () => { it('does not render the path navigation', () => {
expect(findPathNavigation().exists()).toBe(false); expect(findPathNavigation().exists()).toBe(false);
}); });
......
...@@ -2,39 +2,23 @@ import axios from 'axios'; ...@@ -2,39 +2,23 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions'; import * as actions from '~/cycle_analytics/store/actions';
import * as getters from '~/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { allowedStages, selectedStage, selectedValueStream } from '../mock_data'; import { allowedStages, selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path'; const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams'; const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockStartDate = 30; const mockStartDate = 30;
const mockRequestedDataActions = ['fetchValueStreams', 'fetchCycleAnalyticsData']; const mockEndpoints = { fullPath: mockFullPath, requestPath: mockRequestPath };
const mockInitializeActionCommit = {
payload: { requestPath: mockRequestPath },
type: 'INITIALIZE_VSA',
};
const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' }; const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
const mockRequestedDataMutations = [
{
payload: true,
type: 'SET_LOADING',
},
{
payload: false,
type: 'SET_LOADING',
},
];
const features = { const defaultState = { ...getters, selectedValueStream };
cycleAnalyticsForGroups: true,
};
describe('Project Value Stream Analytics actions', () => { describe('Project Value Stream Analytics actions', () => {
let state; let state;
let mock; let mock;
beforeEach(() => { beforeEach(() => {
state = {};
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -45,28 +29,62 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -45,28 +29,62 @@ describe('Project Value Stream Analytics actions', () => {
const mutationTypes = (arr) => arr.map(({ type }) => type); const mutationTypes = (arr) => arr.map(({ type }) => type);
const mockFetchStageDataActions = [
{ type: 'setLoading', payload: true },
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'setLoading', payload: false },
];
describe.each` describe.each`
action | payload | expectedActions | expectedMutations action | payload | expectedActions | expectedMutations
${'initializeVsa'} | ${{ requestPath: mockRequestPath }} | ${mockRequestedDataActions} | ${[mockInitializeActionCommit, ...mockRequestedDataMutations]} ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockRequestedDataActions} | ${[mockSetDateActionCommit, ...mockRequestedDataMutations]} ${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
${'setSelectedStage'} | ${{ selectedStage }} | ${['fetchStageData']} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]} ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${['fetchValueStreamStages']} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]} ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => { `('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations); const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () => it(`will dispatch ${expectedActions} and commit ${types}`, () =>
testAction({ testAction({
action: actions[action], action: actions[action],
state, state,
payload, payload,
expectedMutations, expectedMutations,
expectedActions: expectedActions.map((a) => ({ type: a })), expectedActions,
})); }));
}); });
describe('initializeVsa', () => {
let mockDispatch;
let mockCommit;
const payload = { endpoints: mockEndpoints };
beforeEach(() => {
mockDispatch = jest.fn(() => Promise.resolve());
mockCommit = jest.fn();
});
it('will dispatch the setLoading and fetchValueStreams actions and commit INITIALIZE_VSA', async () => {
await actions.initializeVsa(
{
...state,
dispatch: mockDispatch,
commit: mockCommit,
},
payload,
);
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
});
});
describe('fetchCycleAnalyticsData', () => { describe('fetchCycleAnalyticsData', () => {
beforeEach(() => { beforeEach(() => {
state = { requestPath: mockRequestPath }; state = { endpoints: mockEndpoints };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockRequestPath).reply(httpStatusCodes.OK); mock.onGet(mockRequestPath).reply(httpStatusCodes.OK);
}); });
...@@ -85,7 +103,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -85,7 +103,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => { describe('with a failing request', () => {
beforeEach(() => { beforeEach(() => {
state = { requestPath: mockRequestPath }; state = { endpoints: mockEndpoints };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockRequestPath).reply(httpStatusCodes.BAD_REQUEST); mock.onGet(mockRequestPath).reply(httpStatusCodes.BAD_REQUEST);
}); });
...@@ -105,11 +123,12 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -105,11 +123,12 @@ describe('Project Value Stream Analytics actions', () => {
}); });
describe('fetchStageData', () => { describe('fetchStageData', () => {
const mockStagePath = `${mockRequestPath}/events/${selectedStage.name}`; const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/;
beforeEach(() => { beforeEach(() => {
state = { state = {
requestPath: mockRequestPath, ...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate, startDate: mockStartDate,
selectedStage, selectedStage,
}; };
...@@ -131,7 +150,8 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -131,7 +150,8 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
requestPath: mockRequestPath, ...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate, startDate: mockStartDate,
selectedStage, selectedStage,
}; };
...@@ -155,7 +175,8 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -155,7 +175,8 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => { describe('with a failing request', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
requestPath: mockRequestPath, ...defaultState,
endpoints: mockEndpoints,
startDate: mockStartDate, startDate: mockStartDate,
selectedStage, selectedStage,
}; };
...@@ -179,8 +200,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -179,8 +200,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
features, endpoints: mockEndpoints,
fullPath: mockFullPath,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK); mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
...@@ -199,26 +219,6 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -199,26 +219,6 @@ describe('Project Value Stream Analytics actions', () => {
], ],
})); }));
describe('with cycleAnalyticsForGroups=false', () => {
beforeEach(() => {
state = {
features: { cycleAnalyticsForGroups: false },
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
});
it("does not dispatch the 'fetchStageMedians' request", () =>
testAction({
action: actions.fetchValueStreams,
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
}));
});
describe('with a failing request', () => { describe('with a failing request', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -271,7 +271,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -271,7 +271,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => { beforeEach(() => {
state = { state = {
fullPath: mockFullPath, endpoints: mockEndpoints,
selectedValueStream, selectedValueStream,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
......
...@@ -21,15 +21,12 @@ const convertedEvents = issueEvents.events; ...@@ -21,15 +21,12 @@ const convertedEvents = issueEvents.events;
const mockRequestPath = 'fake/request/path'; const mockRequestPath = 'fake/request/path';
const mockCreatedAfter = '2020-06-18'; const mockCreatedAfter = '2020-06-18';
const mockCreatedBefore = '2020-07-18'; const mockCreatedBefore = '2020-07-18';
const features = {
cycleAnalyticsForGroups: true,
};
describe('Project Value Stream Analytics mutations', () => { describe('Project Value Stream Analytics mutations', () => {
useFakeDate(2020, 6, 18); useFakeDate(2020, 6, 18);
beforeEach(() => { beforeEach(() => {
state = { features }; state = {};
}); });
afterEach(() => { afterEach(() => {
...@@ -61,24 +58,44 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -61,24 +58,44 @@ describe('Project Value Stream Analytics mutations', () => {
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => { `('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, {}); mutations[mutation](state);
expect(state).toMatchObject({ [stateKey]: value }); expect(state).toMatchObject({ [stateKey]: value });
}); });
const mockInitialPayload = {
endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' },
id: 1337,
};
const mockInitializedObj = {
endpoints: { requestPath: mockRequestPath },
createdAfter: mockCreatedAfter,
createdBefore: mockCreatedBefore,
};
it.each`
mutation | stateKey | value
${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }}
${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter}
${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore}
`('$mutation will set $stateKey', ({ mutation, stateKey, value }) => {
mutations[mutation](state, { ...mockInitialPayload });
expect(state).toMatchObject({ ...mockInitializedObj, [stateKey]: value });
});
it.each` it.each`
mutation | payload | stateKey | value mutation | payload | stateKey | value
${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath} ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'startDate'} | ${DEFAULT_DAYS_TO_DISPLAY} ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdAfter'} | ${mockCreatedAfter} ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false} ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary} ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
${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_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians} ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
`( `(
'$mutation with $payload will set $stateKey to $value', '$mutation with $payload will set $stateKey to $value',
...@@ -98,40 +115,9 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -98,40 +115,9 @@ describe('Project Value Stream Analytics mutations', () => {
it.each` it.each`
mutation | payload | stateKey | value mutation | payload | stateKey | value
${types.RECEIVE_STAGE_DATA_SUCCESS} | ${{ events: [] }} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${[]} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_SUCCESS} | ${{ events: rawEvents }} | ${'selectedStageEvents'} | ${convertedEvents} ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${rawEvents} | ${'selectedStageEvents'} | ${convertedEvents}
${types.RECEIVE_STAGE_DATA_SUCCESS} | ${{ events: rawEvents }} | ${'isEmptyStage'} | ${false} ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${rawEvents} | ${'isEmptyStage'} | ${false}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
mutations[mutation](state, payload);
expect(state).toMatchObject({ [stateKey]: value });
},
);
});
describe('with cycleAnalyticsForGroups=false', () => {
useFakeDate(2020, 6, 18);
beforeEach(() => {
state = { features: { cycleAnalyticsForGroups: false } };
});
const formattedMedians = {
code: '2d',
issue: '-',
plan: '21h',
review: '-',
staging: '2d',
test: '4h',
};
it.each`
mutation | payload | stateKey | value
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'medians'} | ${formattedMedians}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${{}} | ${'medians'} | ${{}}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${{}} | ${'medians'} | ${{}}
`( `(
'$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