Commit 552bcfa3 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Mark Florian

Adds dedicated endpoints for project VSA

Adds requests for the dedicated project level
VSA endpoints to the api/analytics_api file.

Updates the loading indicators for the base vue
parent dd3d6bd2
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,
requestPath, endpoints: {
fullPath, requestPath,
features: { fullPath,
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 = [
{ const defaultState = { ...getters, selectedValueStream };
payload: true,
type: 'SET_LOADING',
},
{
payload: false,
type: 'SET_LOADING',
},
];
const features = {
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,25 +58,45 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -61,25 +58,45 @@ 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` it.each`
mutation | payload | stateKey | value mutation | stateKey | value
${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath} ${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'startDate'} | ${DEFAULT_DAYS_TO_DISPLAY} ${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdAfter'} | ${mockCreatedAfter} ${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdBefore'} | ${mockCreatedBefore} `('$mutation will set $stateKey', ({ mutation, stateKey, value }) => {
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} mutations[mutation](state, { ...mockInitialPayload });
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} expect(state).toMatchObject({ ...mockInitializedObj, [stateKey]: value });
${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} it.each`
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]} mutation | payload | stateKey | value
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians} ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
${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}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
`( `(
'$mutation with $payload will set $stateKey to $value', '$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => { ({ mutation, payload, stateKey, value }) => {
...@@ -97,41 +114,10 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -97,41 +114,10 @@ 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