Commit 6ea201af authored by Phil Hughes's avatar Phil Hughes

Merge branch '221204-update-stage-endpoints-for-value-stream' into 'master'

Update stages endpoints to include value stream id

See merge request gitlab-org/gitlab!37047
parents 665a5d43 79a8cb9c
...@@ -14,6 +14,7 @@ import { mapState, mapActions } from 'vuex'; ...@@ -14,6 +14,7 @@ import { mapState, mapActions } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { DATA_REFETCH_DELAY } from '../../shared/constants'; import { DATA_REFETCH_DELAY } from '../../shared/constants';
import { DEFAULT_VALUE_STREAM_ID } from '../constants';
const ERRORS = { const ERRORS = {
MIN_LENGTH: __('Name is required'), MIN_LENGTH: __('Name is required'),
...@@ -31,6 +32,10 @@ const validate = ({ name }) => { ...@@ -31,6 +32,10 @@ const validate = ({ name }) => {
return errors; return errors;
}; };
const hasCustomValueStream = vs => {
return Boolean(vs.length > 1 || vs[0].name.toLowerCase().trim() !== DEFAULT_VALUE_STREAM_ID);
};
export default { export default {
components: { components: {
GlButton, GlButton,
...@@ -48,7 +53,7 @@ export default { ...@@ -48,7 +53,7 @@ export default {
data() { data() {
return { return {
name: '', name: '',
errors: { name: [] }, errors: {},
}; };
}, },
computed: { computed: {
...@@ -59,13 +64,13 @@ export default { ...@@ -59,13 +64,13 @@ export default {
selectedValueStream: 'selectedValueStream', selectedValueStream: 'selectedValueStream',
}), }),
isValid() { isValid() {
return !this.errors?.name.length; return !this.errors.name?.length;
}, },
invalidFeedback() { invalidFeedback() {
return this.errors?.name.join('\n'); return this.errors.name?.join('\n');
}, },
hasValueStreams() { hasValueStreams() {
return Boolean(this.data.length); return Boolean(this.data.length && hasCustomValueStream(this.data));
}, },
selectedValueStreamName() { selectedValueStreamName() {
return this.selectedValueStream?.name || ''; return this.selectedValueStream?.name || '';
...@@ -73,10 +78,19 @@ export default { ...@@ -73,10 +78,19 @@ export default {
selectedValueStreamId() { selectedValueStreamId() {
return this.selectedValueStream?.id || null; return this.selectedValueStream?.id || null;
}, },
hasFormErrors() {
const { initialFormErrors } = this;
return Boolean(Object.keys(initialFormErrors).length);
},
},
watch: {
initialFormErrors(newErrors = {}) {
this.errors = newErrors;
},
}, },
mounted() { mounted() {
const { initialFormErrors } = this; const { initialFormErrors } = this;
if (Object.keys(initialFormErrors).length) { if (this.hasFormErrors) {
this.errors = initialFormErrors; this.errors = initialFormErrors;
} else { } else {
this.onHandleInput(); this.onHandleInput();
...@@ -87,10 +101,12 @@ export default { ...@@ -87,10 +101,12 @@ export default {
onSubmit() { onSubmit() {
const { name } = this; const { name } = this;
return this.createValueStream({ name }).then(() => { return this.createValueStream({ name }).then(() => {
this.$toast.show(sprintf(__("'%{name}' Value Stream created"), { name }), { if (!this.hasFormErrors) {
position: 'top-center', this.$toast.show(sprintf(__("'%{name}' Value Stream created"), { name }), {
}); position: 'top-center',
this.name = ''; });
this.name = '';
}
}); });
}, },
onHandleInput: debounce(function debouncedValidation() { onHandleInput: debounce(function debouncedValidation() {
......
...@@ -76,3 +76,5 @@ export const CAPITALIZED_STAGE_NAME = Object.keys(STAGE_NAME).reduce((acc, stage ...@@ -76,3 +76,5 @@ export const CAPITALIZED_STAGE_NAME = Object.keys(STAGE_NAME).reduce((acc, stage
}, {}); }, {});
export const PATH_HOME_ICON = 'home'; export const PATH_HOME_ICON = 'home';
export const DEFAULT_VALUE_STREAM_ID = 'default';
...@@ -112,7 +112,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => { ...@@ -112,7 +112,6 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
return Promise.resolve() return Promise.resolve()
.then(() => dispatch('fetchValueStreams')) .then(() => dispatch('fetchValueStreams'))
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues')) .then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('receiveCycleAnalyticsDataSuccess')) .then(() => dispatch('receiveCycleAnalyticsDataSuccess'))
.catch(error => dispatch('receiveCycleAnalyticsDataError', error)); .catch(error => dispatch('receiveCycleAnalyticsDataError', error));
...@@ -144,20 +143,35 @@ export const receiveGroupStagesSuccess = ({ commit, dispatch }, stages) => { ...@@ -144,20 +143,35 @@ export const receiveGroupStagesSuccess = ({ commit, dispatch }, stages) => {
return dispatch('setDefaultSelectedStage'); return dispatch('setDefaultSelectedStage');
}; };
export const fetchValueStreamStages = ({
hasCreateMultipleValueStreams,
valueStreamId,
groupId,
params,
}) => {
return hasCreateMultipleValueStreams
? Api.cycleAnalyticsValueStreamGroupStagesAndEvents(groupId, valueStreamId, params)
: Api.cycleAnalyticsGroupStagesAndEvents(groupId, params);
};
export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
const { const {
selectedGroup: { fullPath }, featureFlags: { hasCreateMultipleValueStreams = false },
} = state; } = state;
const { const {
currentValueStreamId: valueStreamId,
currentGroupPath: groupId,
cycleAnalyticsRequestParams: { created_after, project_ids }, cycleAnalyticsRequestParams: { created_after, project_ids },
} = getters; } = getters;
dispatch('requestGroupStages'); dispatch('requestGroupStages');
dispatch('customStages/setStageEvents', []); dispatch('customStages/setStageEvents', []);
return Api.cycleAnalyticsGroupStagesAndEvents(fullPath, { return fetchValueStreamStages({
start_date: created_after, hasCreateMultipleValueStreams,
project_ids, groupId,
valueStreamId,
params: { start_date: created_after, project_ids },
}) })
.then(({ data: { stages = [], events = [] } }) => { .then(({ data: { stages = [], events = [] } }) => {
dispatch('receiveGroupStagesSuccess', stages); dispatch('receiveGroupStagesSuccess', stages);
...@@ -307,13 +321,15 @@ export const createValueStream = ({ commit, dispatch, rootState }, data) => { ...@@ -307,13 +321,15 @@ export const createValueStream = ({ commit, dispatch, rootState }, data) => {
return Api.cycleAnalyticsCreateValueStream(fullPath, data) return Api.cycleAnalyticsCreateValueStream(fullPath, data)
.then(() => dispatch('receiveCreateValueStreamSuccess')) .then(() => dispatch('receiveCreateValueStreamSuccess'))
.catch(({ response } = {}) => { .catch(({ response } = {}) => {
const { data: { message, errors } = null } = response; const { data: { message, payload: { errors } } = null } = response;
commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { data, message, errors }); commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors });
}); });
}; };
export const setSelectedValueStream = ({ commit }, streamId) => export const setSelectedValueStream = ({ commit, dispatch }, streamId) => {
commit(types.SET_SELECTED_VALUE_STREAM, streamId); commit(types.SET_SELECTED_VALUE_STREAM, streamId);
return dispatch('fetchGroupStagesAndEvents');
};
export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data); commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
...@@ -340,5 +356,5 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => { ...@@ -340,5 +356,5 @@ export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, data); commit(types.RECEIVE_VALUE_STREAMS_ERROR, data);
}); });
} }
return Promise.resolve(); return dispatch('fetchGroupStagesAndEvents');
}; };
...@@ -65,15 +65,17 @@ export const receiveCreateStageError = ( ...@@ -65,15 +65,17 @@ export const receiveCreateStageError = (
return dispatch('setStageFormErrors', errors); return dispatch('setStageFormErrors', errors);
}; };
export const createStage = ({ dispatch, rootState }, data) => { export const createStage = ({ dispatch, rootState, rootGetters }, data) => {
const { const {
selectedGroup: { fullPath }, selectedGroup: { fullPath },
} = rootState; } = rootState;
const { currentValueStreamId } = rootGetters;
dispatch('clearFormErrors'); dispatch('clearFormErrors');
dispatch('setSavingCustomStage'); dispatch('setSavingCustomStage');
return Api.cycleAnalyticsCreateStage(fullPath, data) return Api.cycleAnalyticsCreateStage(fullPath, currentValueStreamId, data)
.then(response => { .then(response => {
const { status, data: responseData } = response; const { status, data: responseData } = response;
return dispatch('receiveCreateStageSuccess', { status, data: responseData }); return dispatch('receiveCreateStageSuccess', { status, data: responseData });
......
...@@ -126,7 +126,7 @@ export default { ...@@ -126,7 +126,7 @@ export default {
state.isCreatingValueStream = true; state.isCreatingValueStream = true;
state.createValueStreamErrors = {}; state.createValueStreamErrors = {};
}, },
[types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, errors = {}) { [types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, { errors } = {}) {
state.isCreatingValueStream = false; state.isCreatingValueStream = false;
state.createValueStreamErrors = errors; state.createValueStreamErrors = errors;
}, },
......
...@@ -144,6 +144,14 @@ export default { ...@@ -144,6 +144,14 @@ export default {
return axios.get(url, { params }); return axios.get(url, { params });
}, },
cycleAnalyticsValueStreamGroupStagesAndEvents(groupId, valueStreamId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsValueStreamGroupStagesAndEventsPath)
.replace(':id', groupId)
.replace(':value_stream_id', valueStreamId);
return axios.get(url, { params });
},
cycleAnalyticsStageEvents(groupId, stageId, params = {}) { cycleAnalyticsStageEvents(groupId, stageId, params = {}) {
const url = Api.buildUrl(this.cycleAnalyticsStageEventsPath) const url = Api.buildUrl(this.cycleAnalyticsStageEventsPath)
.replace(':id', groupId) .replace(':id', groupId)
...@@ -160,8 +168,10 @@ export default { ...@@ -160,8 +168,10 @@ export default {
return axios.get(url, { params: { ...params } }); return axios.get(url, { params: { ...params } });
}, },
cycleAnalyticsCreateStage(groupId, data) { cycleAnalyticsCreateStage(groupId, valueStreamId, data) {
const url = Api.buildUrl(this.cycleAnalyticsGroupStagesAndEventsPath).replace(':id', groupId); const url = Api.buildUrl(this.cycleAnalyticsValueStreamGroupStagesAndEventsPath)
.replace(':id', groupId)
.replace(':value_stream_id', valueStreamId);
return axios.post(url, data); return axios.post(url, data);
}, },
......
...@@ -126,7 +126,7 @@ module Analytics ...@@ -126,7 +126,7 @@ module Analytics
end end
def load_value_stream def load_value_stream
if params[:value_stream_id] if params[:value_stream_id] && params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
@value_stream = @group.value_streams.find(params[:value_stream_id]) @value_stream = @group.value_streams.find(params[:value_stream_id])
end end
end end
......
...@@ -11,7 +11,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Analytics::App ...@@ -11,7 +11,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Analytics::App
end end
def index def index
render json: Analytics::GroupValueStreamSerializer.new.represent(@group.value_streams) render json: Analytics::GroupValueStreamSerializer.new.represent(value_streams)
end end
def create def create
...@@ -29,4 +29,12 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Analytics::App ...@@ -29,4 +29,12 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Analytics::App
def value_stream_params def value_stream_params
params.require(:value_stream).permit(:name) params.require(:value_stream).permit(:name)
end end
def value_streams
@group.value_streams.presence || [in_memory_default_value_stream]
end
def in_memory_default_value_stream
@group.value_streams.new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME)
end
end end
...@@ -4,5 +4,11 @@ module Analytics ...@@ -4,5 +4,11 @@ module Analytics
class GroupValueStreamEntity < Grape::Entity class GroupValueStreamEntity < Grape::Entity
expose :name expose :name
expose :id expose :id
private
def id
object.id || object.name # use the name `default` if the record is not persisted
end
end end
end end
...@@ -54,6 +54,19 @@ RSpec.describe Analytics::CycleAnalytics::StagesController do ...@@ -54,6 +54,19 @@ RSpec.describe Analytics::CycleAnalytics::StagesController do
expect(response).to be_successful expect(response).to be_successful
end end
context 'when `default` value_stream_id is given' do
before do
params[:value_stream_id] = Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
it 'returns only the default value stream stages' do
subject
expect(response).to be_successful
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end
end
it 'renders `forbidden` based on the response of the service object' do it 'renders `forbidden` based on the response of the service object' do
expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false) expect_any_instance_of(Analytics::CycleAnalytics::Stages::ListService).to receive(:can?).and_return(false)
......
...@@ -6,7 +6,6 @@ RSpec.describe Groups::Analytics::CycleAnalytics::ValueStreamsController do ...@@ -6,7 +6,6 @@ RSpec.describe Groups::Analytics::CycleAnalytics::ValueStreamsController do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group, refind: true) { create(:group) } let_it_be(:group, refind: true) { create(:group) }
let(:params) { { group_id: group } } let(:params) { { group_id: group } }
let!(:value_stream) { create(:cycle_analytics_group_value_stream, group: group) }
before do before do
stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true) stub_feature_flags(Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG => true)
...@@ -17,11 +16,27 @@ RSpec.describe Groups::Analytics::CycleAnalytics::ValueStreamsController do ...@@ -17,11 +16,27 @@ RSpec.describe Groups::Analytics::CycleAnalytics::ValueStreamsController do
end end
describe 'GET #index' do describe 'GET #index' do
it 'succeeds' do it 'returns an in-memory default value stream' do
get :index, params: params get :index, params: params
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/cycle_analytics/value_streams', dir: 'ee')
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME)
expect(json_response.first['name']).to eq(Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME)
end
context 'when persisted value streams present' do
let!(:value_stream) { create(:cycle_analytics_group_value_stream, group: group) }
it 'succeeds' do
get :index, params: params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('analytics/cycle_analytics/value_streams', dir: 'ee')
expect(json_response.first['id']).to eq(value_stream.id)
expect(json_response.first['name']).to eq(value_stream.name)
end
end end
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"required": ["name", "id"], "required": ["name", "id"],
"properties": { "properties": {
"id": { "id": {
"type": "integer" "type": ["integer", "string"]
}, },
"name": { "name": {
"type": "string" "type": "string"
......
...@@ -179,7 +179,7 @@ describe('Cycle Analytics component', () => { ...@@ -179,7 +179,7 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(FilterBar).exists()).toBe(flag); expect(wrapper.find(FilterBar).exists()).toBe(flag);
}; };
const displaysCreateValueStream = flag => { const displaysValueStreamSelect = flag => {
expect(wrapper.find(ValueStreamSelect).exists()).toBe(flag); expect(wrapper.find(ValueStreamSelect).exists()).toBe(flag);
}; };
...@@ -247,8 +247,8 @@ describe('Cycle Analytics component', () => { ...@@ -247,8 +247,8 @@ describe('Cycle Analytics component', () => {
displaysPathNavigation(false); displaysPathNavigation(false);
}); });
it('does not display the create multiple value streams button', () => { it('does not display the value stream select component', () => {
displaysCreateValueStream(false); displaysValueStreamSelect(false);
}); });
describe('hideGroupDropDown = true', () => { describe('hideGroupDropDown = true', () => {
...@@ -276,8 +276,8 @@ describe('Cycle Analytics component', () => { ...@@ -276,8 +276,8 @@ describe('Cycle Analytics component', () => {
}); });
}); });
it('displays the create multiple value streams button', () => { it('displays the value stream select component', () => {
displaysCreateValueStream(true); displaysValueStreamSelect(true);
}); });
}); });
}); });
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlModal, GlNewDropdown as GlDropdown } from '@gitlab/ui'; import { GlButton, GlModal, GlNewDropdown as GlDropdown, GlFormGroup } from '@gitlab/ui';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue'; import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import { valueStreams } from '../mock_data'; import { valueStreams } from '../mock_data';
import { findDropdownItemText } from '../helpers'; import { findDropdownItemText } from '../helpers';
...@@ -14,6 +14,7 @@ describe('ValueStreamSelect', () => { ...@@ -14,6 +14,7 @@ describe('ValueStreamSelect', () => {
const createValueStreamMock = jest.fn(() => Promise.resolve()); const createValueStreamMock = jest.fn(() => Promise.resolve());
const mockEvent = { preventDefault: jest.fn() }; const mockEvent = { preventDefault: jest.fn() };
const mockToastShow = jest.fn(); const mockToastShow = jest.fn();
const streamName = 'Cool stream';
const fakeStore = ({ initialState = {} }) => const fakeStore = ({ initialState = {} }) =>
new Vuex.Store({ new Vuex.Store({
...@@ -52,6 +53,7 @@ describe('ValueStreamSelect', () => { ...@@ -52,6 +53,7 @@ describe('ValueStreamSelect', () => {
const findSelectValueStreamDropdown = () => wrapper.find(GlDropdown); const findSelectValueStreamDropdown = () => wrapper.find(GlDropdown);
const findSelectValueStreamDropdownOptions = _wrapper => findDropdownItemText(_wrapper); const findSelectValueStreamDropdownOptions = _wrapper => findDropdownItemText(_wrapper);
const findCreateValueStreamButton = () => wrapper.find(GlButton); const findCreateValueStreamButton = () => wrapper.find(GlButton);
const findFormGroup = () => wrapper.find(GlFormGroup);
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
...@@ -105,9 +107,30 @@ describe('ValueStreamSelect', () => { ...@@ -105,9 +107,30 @@ describe('ValueStreamSelect', () => {
expect(submitButtonDisabledState()).toBe(true); expect(submitButtonDisabledState()).toBe(true);
}); });
describe('with valid fields', () => { describe('form errors', () => {
const streamName = 'Cool stream'; const fieldErrors = ['already exists', 'is required'];
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors: {
name: fieldErrors,
},
},
});
});
it('renders the error', () => {
expect(findFormGroup().attributes('invalid-feedback')).toEqual(fieldErrors.join('\n'));
});
it('submit button is disabled', () => {
expect(submitButtonDisabledState()).toBe(true);
});
});
describe('with valid fields', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ data: { name: streamName } }); wrapper = createComponent({ data: { name: streamName } });
}); });
......
...@@ -62,11 +62,10 @@ describe('Cycle analytics actions', () => { ...@@ -62,11 +62,10 @@ describe('Cycle analytics actions', () => {
}); });
it.each` it.each`
action | type | stateKey | payload action | type | stateKey | payload
${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }} ${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]} ${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }} ${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }}
${'setSelectedValueStream'} | ${'SET_SELECTED_VALUE_STREAM'} | ${'selectedValueStream'} | ${{ id: 'vs-1', name: 'Value stream 1' }}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => { `('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
return testAction( return testAction(
actions[action], actions[action],
...@@ -82,6 +81,20 @@ describe('Cycle analytics actions', () => { ...@@ -82,6 +81,20 @@ describe('Cycle analytics actions', () => {
); );
}); });
describe('setSelectedValueStream', () => {
const vs = { id: 'vs-1', name: 'Value stream 1' };
it('dispatches the fetchCycleAnalyticsData action', () => {
return testAction(
actions.setSelectedValueStream,
vs,
{ ...state, selectedValueStream: {} },
[{ type: types.SET_SELECTED_VALUE_STREAM, payload: vs }],
[{ type: 'fetchGroupStagesAndEvents' }],
);
});
});
describe('setDateRange', () => { describe('setDateRange', () => {
const payload = { startDate, endDate }; const payload = { startDate, endDate };
...@@ -256,7 +269,6 @@ describe('Cycle analytics actions', () => { ...@@ -256,7 +269,6 @@ describe('Cycle analytics actions', () => {
[ [
{ type: 'requestCycleAnalyticsData' }, { type: 'requestCycleAnalyticsData' },
{ type: 'fetchValueStreams' }, { type: 'fetchValueStreams' },
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' }, { type: 'fetchStageMedianValues' },
{ type: 'receiveCycleAnalyticsDataSuccess' }, { type: 'receiveCycleAnalyticsDataSuccess' },
], ],
...@@ -910,7 +922,9 @@ describe('Cycle analytics actions', () => { ...@@ -910,7 +922,9 @@ describe('Cycle analytics actions', () => {
}); });
describe('with errors', () => { describe('with errors', () => {
const resp = { message: 'error', errors: {} }; const errors = { name: ['is taken'] };
const message = { message: 'error' };
const resp = { message, payload: { errors } };
beforeEach(() => { beforeEach(() => {
mock.onPost(endpoints.valueStreamData).replyOnce(httpStatusCodes.NOT_FOUND, resp); mock.onPost(endpoints.valueStreamData).replyOnce(httpStatusCodes.NOT_FOUND, resp);
}); });
...@@ -924,7 +938,7 @@ describe('Cycle analytics actions', () => { ...@@ -924,7 +938,7 @@ describe('Cycle analytics actions', () => {
{ type: types.REQUEST_CREATE_VALUE_STREAM }, { type: types.REQUEST_CREATE_VALUE_STREAM },
{ {
type: types.RECEIVE_CREATE_VALUE_STREAM_ERROR, type: types.RECEIVE_CREATE_VALUE_STREAM_ERROR,
payload: { data: { ...payload }, ...resp }, payload: { message, errors },
}, },
], ],
[], [],
...@@ -1016,8 +1030,14 @@ describe('Cycle analytics actions', () => { ...@@ -1016,8 +1030,14 @@ describe('Cycle analytics actions', () => {
}; };
}); });
it(`will skip making a request`, () => it(`will dispatch the 'fetchGroupStagesAndEvents' request`, () =>
testAction(actions.fetchValueStreams, null, state, [], [])); testAction(
actions.fetchValueStreams,
null,
state,
[],
[{ type: 'fetchGroupStagesAndEvents' }],
));
}); });
}); });
}); });
...@@ -57,15 +57,15 @@ describe('Cycle analytics mutations', () => { ...@@ -57,15 +57,15 @@ describe('Cycle analytics mutations', () => {
}); });
it.each` it.each`
mutation | payload | expectedState mutation | payload | expectedState
${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }} ${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }}
${types.SET_SELECTED_GROUP} | ${{ fullPath: 'cool-beans' }} | ${{ selectedGroup: { fullPath: 'cool-beans' }, selectedProjects: [] }} ${types.SET_SELECTED_GROUP} | ${{ fullPath: 'cool-beans' }} | ${{ selectedGroup: { fullPath: 'cool-beans' }, selectedProjects: [] }}
${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }} ${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }} ${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }} ${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }}
${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${{ name: ['is required'] }} | ${{ createValueStreamErrors: { name: ['is required'] }, isCreatingValueStream: false }} ${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${{ errors: { name: ['is required'] } }} | ${{ createValueStreamErrors: { name: ['is required'] }, isCreatingValueStream: false }}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }} ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }}
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }} ${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }}
`( `(
'$mutation with payload $payload will update state with $expectedState', '$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => { ({ mutation, payload, expectedState }) => {
......
...@@ -442,6 +442,8 @@ describe('Api', () => { ...@@ -442,6 +442,8 @@ describe('Api', () => {
}); });
describe('cycleAnalyticsCreateStage', () => { describe('cycleAnalyticsCreateStage', () => {
const valueStreamId = 'fake-value-stream';
it('submit the custom stage data', done => { it('submit the custom stage data', done => {
const response = {}; const response = {};
const customStage = { const customStage = {
...@@ -451,10 +453,10 @@ describe('Api', () => { ...@@ -451,10 +453,10 @@ describe('Api', () => {
end_event_identifier: 'issue_closed', end_event_identifier: 'issue_closed',
end_event_label_id: null, end_event_label_id: null,
}; };
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/stages`; const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/value_streams/${valueStreamId}/stages`;
mock.onPost(expectedUrl).reply(httpStatus.OK, response); mock.onPost(expectedUrl).reply(httpStatus.OK, response);
Api.cycleAnalyticsCreateStage(groupId, customStage) Api.cycleAnalyticsCreateStage(groupId, valueStreamId, customStage)
.then(({ data, config: { data: reqData, url } }) => { .then(({ data, config: { data: reqData, url } }) => {
expect(data).toEqual(response); expect(data).toEqual(response);
expect(JSON.parse(reqData)).toMatchObject(customStage); expect(JSON.parse(reqData)).toMatchObject(customStage);
......
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