Commit 84f4d0e8 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '221204-fe-multiple-value-streams-switch-between-value-streams-mvc' into 'master'

Switch between value streams MVC

See merge request gitlab-org/gitlab!35729
parents 59c288db 980bb468
......@@ -79,6 +79,7 @@ export default {
'startDate',
'endDate',
'medians',
'isLoadingValueStreams',
]),
// NOTE: formEvents are fetched in the same request as the list of stages (fetchGroupStagesAndEvents)
// so i think its ok to bind formEvents here even though its only used as a prop to the custom-stage-form
......@@ -114,7 +115,9 @@ export default {
return this.featureFlags.hasFilterBar && this.currentGroupPath;
},
shouldDisplayCreateMultipleValueStreams() {
return Boolean(this.featureFlags.hasCreateMultipleValueStreams);
return Boolean(
this.featureFlags.hasCreateMultipleValueStreams && !this.isLoadingValueStreams,
);
},
isLoadingTypeOfWork() {
return this.isLoadingTasksByTypeChartTopLabels || this.isLoadingTasksByTypeChart;
......
<script>
import { GlButton, GlForm, GlFormInput, GlFormGroup, GlModal, GlModalDirective } from '@gitlab/ui';
import {
GlButton,
GlNewDropdown as GlDropdown,
GlNewDropdownItem as GlDropdownItem,
GlNewDropdownDivider as GlDropdownDivider,
GlForm,
GlFormInput,
GlFormGroup,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { sprintf, __ } from '~/locale';
import { debounce } from 'lodash';
import { DATA_REFETCH_DELAY } from '../../shared/constants';
const ERRORS = {
MIN_LENGTH: __('Name is required'),
......@@ -23,6 +34,9 @@ const validate = ({ name }) => {
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlForm,
GlFormInput,
GlFormGroup,
......@@ -41,6 +55,8 @@ export default {
...mapState({
isLoading: 'isCreatingValueStream',
initialFormErrors: 'createValueStreamErrors',
data: 'valueStreams',
selectedValueStream: 'selectedValueStream',
}),
isValid() {
return !this.errors?.name.length;
......@@ -48,6 +64,15 @@ export default {
invalidFeedback() {
return this.errors?.name.join('\n');
},
hasValueStreams() {
return Boolean(this.data.length);
},
selectedValueStreamName() {
return this.selectedValueStream?.name || '';
},
selectedValueStreamId() {
return this.selectedValueStream?.id || null;
},
},
mounted() {
const { initialFormErrors } = this;
......@@ -58,11 +83,10 @@ export default {
}
},
methods: {
...mapActions(['createValueStream']),
...mapActions(['createValueStream', 'setSelectedValueStream']),
onSubmit() {
const { name } = this;
return this.createValueStream({ name }).then(() => {
this.$refs.modal.hide();
this.$toast.show(sprintf(__("'%{name}' Value Stream created"), { name }), {
position: 'top-center',
});
......@@ -72,23 +96,43 @@ export default {
onHandleInput: debounce(function debouncedValidation() {
const { name } = this;
this.errors = validate({ name });
}, 250),
}, DATA_REFETCH_DELAY),
isSelected(id) {
return Boolean(this.selectedValueStreamId && this.selectedValueStreamId === id);
},
onSelect(id) {
this.setSelectedValueStream(id);
},
},
};
</script>
<template>
<gl-form>
<gl-button v-gl-modal-directive="'create-value-stream-modal'" @click="onHandleInput">{{
__('Create new value stream')
<gl-dropdown v-if="hasValueStreams" :text="selectedValueStreamName" right>
<gl-dropdown-item
v-for="{ id, name: streamName } in data"
:key="id"
:is-check-item="true"
:is-checked="isSelected(id)"
@click="onSelect(id)"
>{{ streamName }}</gl-dropdown-item
>
<gl-dropdown-divider />
<gl-dropdown-item v-gl-modal-directive="'create-value-stream-modal'" @click="onHandleInput">{{
__('Create new Value Stream')
}}</gl-dropdown-item>
</gl-dropdown>
<gl-button v-else v-gl-modal-directive="'create-value-stream-modal'" @click="onHandleInput">{{
__('Create new Value Stream')
}}</gl-button>
<gl-modal
ref="modal"
modal-id="create-value-stream-modal"
:title="__('Value Stream Name')"
:action-primary="{
text: __('Create value stream'),
text: __('Create Value Stream'),
attributes: [
{ variant: 'primary' },
{ variant: 'success' },
{
disabled: !isValid,
},
......@@ -108,7 +152,7 @@ export default {
id="create-value-stream-name"
v-model.trim="name"
name="create-value-stream-name"
:placeholder="__('Example: My value stream')"
:placeholder="__('Example: My Value Stream')"
:state="isValid"
required
@input="onHandleInput"
......
......@@ -109,7 +109,9 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
removeFlash();
dispatch('requestCycleAnalyticsData');
return Promise.resolve()
.then(() => dispatch('fetchValueStreams'))
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchStageMedianValues'))
.then(() => dispatch('receiveCycleAnalyticsDataSuccess'))
......@@ -296,7 +298,12 @@ export const reorderStage = ({ dispatch, state }, initialData) => {
);
};
export const createValueStream = ({ commit, rootState }, data) => {
export const receiveCreateValueStreamSuccess = ({ commit, dispatch }) => {
commit(types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS);
return dispatch('fetchValueStreams');
};
export const createValueStream = ({ commit, dispatch, rootState }, data) => {
const {
selectedGroup: { fullPath },
} = rootState;
......@@ -304,12 +311,40 @@ export const createValueStream = ({ commit, rootState }, data) => {
commit(types.REQUEST_CREATE_VALUE_STREAM);
return Api.cycleAnalyticsCreateValueStream(fullPath, data)
.then(response => {
const { status, data: responseData } = response;
commit(types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, { status, data: responseData });
})
.then(() => dispatch('receiveCreateValueStreamSuccess'))
.catch(({ response } = {}) => {
const { data: { message, errors } = null } = response;
commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { data, message, errors });
});
};
export const setSelectedValueStream = ({ commit }, streamId) =>
commit(types.SET_SELECTED_VALUE_STREAM, streamId);
export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
commit(types.RECEIVE_VALUE_STREAMS_SUCCESS, data);
if (data.length) {
const [firstStream] = data;
return dispatch('setSelectedValueStream', firstStream.id);
}
return Promise.resolve();
};
export const fetchValueStreams = ({ commit, dispatch, getters, state }) => {
const {
featureFlags: { hasCreateMultipleValueStreams = false },
} = state;
const { currentGroupPath } = getters;
if (hasCreateMultipleValueStreams) {
commit(types.REQUEST_VALUE_STREAMS);
return Api.cycleAnalyticsValueStreams(currentGroupPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.catch(response => {
const { data } = response;
commit(types.RECEIVE_VALUE_STREAMS_ERROR, data);
});
}
return Promise.resolve();
};
......@@ -6,6 +6,8 @@ import { transformStagesForPathNavigation } from '../utils';
export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN;
export const currentValueStreamId = ({ selectedValueStream }) => selectedValueStream?.id || null;
export const currentGroupPath = ({ selectedGroup }) =>
selectedGroup && selectedGroup.fullPath ? selectedGroup.fullPath : null;
......
......@@ -5,6 +5,7 @@ export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const SET_SELECTED_FILTERS = 'SET_SELECTED_FILTERS';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
export const RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
......@@ -39,3 +40,7 @@ export const RECEIVE_REORDER_STAGE_ERROR = 'RECEIVE_REORDER_STAGE_ERROR';
export const REQUEST_CREATE_VALUE_STREAM = 'REQUEST_CREATE_VALUE_STREAM';
export const RECEIVE_CREATE_VALUE_STREAM_SUCCESS = 'RECEIVE_CREATE_VALUE_STREAM_SUCCESS';
export const RECEIVE_CREATE_VALUE_STREAM_ERROR = 'RECEIVE_CREATE_VALUE_STREAM_ERROR';
export const REQUEST_VALUE_STREAMS = 'REQUEST_VALUE_STREAMS';
export const RECEIVE_VALUE_STREAMS_SUCCESS = 'RECEIVE_VALUE_STREAMS_SUCCESS';
export const RECEIVE_VALUE_STREAMS_ERROR = 'RECEIVE_VALUE_STREAMS_ERROR';
......@@ -134,4 +134,19 @@ export default {
state.isCreatingValueStream = false;
state.createValueStreamErrors = {};
},
[types.SET_SELECTED_VALUE_STREAM](state, streamId) {
state.selectedValueStream = state.valueStreams?.find(({ id }) => id === streamId) || null;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.isLoadingValueStreams = true;
state.valueStreams = [];
},
[types.RECEIVE_VALUE_STREAMS_ERROR](state) {
state.isLoadingValueStreams = false;
state.valueStreams = [];
},
[types.RECEIVE_VALUE_STREAMS_SUCCESS](state, data) {
state.isLoadingValueStreams = false;
state.valueStreams = data;
},
};
......@@ -20,13 +20,16 @@ export default () => ({
selectedMilestone: null,
selectedAssignees: [],
selectedLabels: [],
selectedValueStream: null,
currentStageEvents: [],
isLoadingValueStreams: false,
isCreatingValueStream: false,
createValueStreamErrors: {},
stages: [],
summary: [],
medians: {},
valueStreams: [],
});
......@@ -15,6 +15,8 @@ export default {
cycleAnalyticsSummaryDataPath: '/groups/:id/-/analytics/value_stream_analytics/summary',
cycleAnalyticsTimeSummaryDataPath: '/groups/:id/-/analytics/value_stream_analytics/time_summary',
cycleAnalyticsGroupStagesAndEventsPath: '/groups/:id/-/analytics/value_stream_analytics/stages',
cycleAnalyticsValueStreamGroupStagesAndEventsPath:
'/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages',
cycleAnalyticsValueStreamsPath: '/groups/:id/-/analytics/value_stream_analytics/value_streams',
cycleAnalyticsStageEventsPath:
'/groups/:id/-/analytics/value_stream_analytics/stages/:stage_id/records',
......@@ -168,6 +170,11 @@ export default {
return axios.post(url, data);
},
cycleAnalyticsValueStreams(groupId, data) {
const url = Api.buildUrl(this.cycleAnalyticsValueStreamsPath).replace(':id', groupId);
return axios.get(url, data);
},
cycleAnalyticsStageUrl(stageId, groupId) {
return Api.buildUrl(this.cycleAnalyticsStagePath)
.replace(':id', groupId)
......
......@@ -4,6 +4,8 @@ module Analytics
module CycleAnalytics
module Stages
class ListService < BaseService
extend ::Gitlab::Utils::Override
def execute
return forbidden unless can?(current_user, :read_group_cycle_analytics, parent)
......@@ -21,6 +23,11 @@ module Analytics
scope = scope.by_value_stream(params[:value_stream]) if params[:value_stream]
scope.for_list
end
override :value_stream
def value_stream
@value_stream ||= (params[:value_stream] || parent.value_streams.new(name: DEFAULT_VALUE_STREAM_NAME))
end
end
end
end
......
......@@ -1019,10 +1019,10 @@ RSpec.describe 'Group Value Stream Analytics', :js do
end
it 'can create a value stream' do
page.find_button(_('Create new value stream')).click
page.find_button(_('Create new Value Stream')).click
fill_in 'create-value-stream-name', with: custom_value_stream_name
page.find_button(_('Create value stream')).click
page.find_button(_('Create Value Stream')).click
expect(page).to have_text(_("'%{name}' Value Stream created") % { name: custom_value_stream_name })
end
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import store from 'ee/analytics/cycle_analytics/store';
import { GlButton, GlModal, GlNewDropdown as GlDropdown } from '@gitlab/ui';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import { valueStreams } from '../mock_data';
import { findDropdownItemText } from '../helpers';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -12,13 +13,23 @@ describe('ValueStreamSelect', () => {
const createValueStreamMock = jest.fn(() => Promise.resolve());
const mockEvent = { preventDefault: jest.fn() };
const mockModalHide = jest.fn();
const mockToastShow = jest.fn();
const createComponent = ({ data = {}, methods = {} } = {}) =>
const fakeStore = ({ initialState = {} }) =>
new Vuex.Store({
state: {
isLoading: false,
createValueStreamErrors: {},
valueStreams: [],
selectedValueStream: {},
...initialState,
},
});
const createComponent = ({ data = {}, initialState = {}, methods = {} } = {}) =>
shallowMount(ValueStreamSelect, {
localVue,
store,
store: fakeStore({ initialState }),
data() {
return {
...data,
......@@ -38,15 +49,57 @@ describe('ValueStreamSelect', () => {
const findModal = () => wrapper.find(GlModal);
const submitButtonDisabledState = () => findModal().props('actionPrimary').attributes[1].disabled;
const submitForm = () => findModal().vm.$emit('primary', mockEvent);
const findSelectValueStreamDropdown = () => wrapper.find(GlDropdown);
const findSelectValueStreamDropdownOptions = _wrapper => findDropdownItemText(_wrapper);
const findCreateValueStreamButton = () => wrapper.find(GlButton);
beforeEach(() => {
wrapper = createComponent();
wrapper = createComponent({
initialState: {
valueStreams,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('with value streams available', () => {
it('does not display the create value stream button', () => {
expect(findCreateValueStreamButton().exists()).toBe(false);
});
it('displays the select value stream dropdown', () => {
expect(findSelectValueStreamDropdown().exists()).toBe(true);
});
it('renders each value stream including a create button', () => {
const opts = findSelectValueStreamDropdownOptions(wrapper);
[...valueStreams.map(v => v.name), 'Create new Value Stream'].forEach(vs => {
expect(opts).toContain(vs);
});
});
});
describe('No value streams available', () => {
beforeEach(() => {
wrapper = createComponent({
initialState: {
valueStreams: [],
},
});
});
it('displays the create value stream button', () => {
expect(findCreateValueStreamButton().exists()).toBe(true);
});
it('does not display the select value stream dropdown', () => {
expect(findSelectValueStreamDropdown().exists()).toBe(false);
});
});
describe('Create value stream form', () => {
it('submit button is disabled', () => {
expect(submitButtonDisabledState()).toBe(true);
......@@ -57,7 +110,6 @@ describe('ValueStreamSelect', () => {
beforeEach(() => {
wrapper = createComponent({ data: { name: streamName } });
wrapper.vm.$refs.modal.hide = mockModalHide;
});
it('submit button is enabled', () => {
......@@ -68,6 +120,7 @@ describe('ValueStreamSelect', () => {
beforeEach(() => {
submitForm();
});
it('calls the "createValueStream" event when submitted', () => {
expect(createValueStreamMock).toHaveBeenCalledWith({ name: streamName });
});
......@@ -81,10 +134,6 @@ describe('ValueStreamSelect', () => {
position: 'top-center',
});
});
it('hides the modal', () => {
expect(mockModalHide).toHaveBeenCalled();
});
});
describe('form submission fails', () => {
......@@ -93,11 +142,10 @@ describe('ValueStreamSelect', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
methods: {
createValueStream: createValueStreamMockFail,
actions: {
createValueStream: () => createValueStreamMockFail,
},
});
wrapper.vm.$refs.modal.hide = mockModalHide;
});
it('does not clear the name field', () => {
......@@ -107,10 +155,6 @@ describe('ValueStreamSelect', () => {
it('does not display a toast message', () => {
expect(mockToastShow).not.toHaveBeenCalled();
});
it('does not hide the modal', () => {
expect(mockModalHide).not.toHaveBeenCalled();
});
});
});
});
......
import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
export function renderTotalTime(selector, element, totalTime = {}) {
const { days, hours, mins, seconds } = totalTime;
if (days) {
......@@ -17,7 +19,14 @@ export function renderTotalTime(selector, element, totalTime = {}) {
export const shouldFlashAMessage = (msg = '') =>
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
export const findDropdownItems = wrapper => wrapper.findAll(GlDropdownItem);
export const findDropdownItemText = wrapper =>
findDropdownItems(wrapper).wrappers.map(w => w.text());
export default {
renderTotalTime,
shouldFlashAMessage,
findDropdownItems,
findDropdownItemText,
};
......@@ -38,6 +38,8 @@ export const endpoints = {
valueStreamData: /analytics\/value_stream_analytics\/value_streams/,
};
export const valueStreams = [{ id: 1, name: 'Value stream 1' }, { id: 2, name: 'Value stream 2' }];
export const groupLabels = getJSONFixture(fixtureEndpoints.groupLabels).map(
convertObjectPropsToCamelCase,
);
......
......@@ -13,6 +13,7 @@ import {
endDate,
customizableStagesAndEvents,
endpoints,
valueStreams,
} from '../mock_data';
const stageData = { events: [] };
......@@ -20,6 +21,7 @@ const error = new Error(`Request failed with status code ${httpStatusCodes.NOT_F
const flashErrorMessage = 'There was an error while fetching value stream analytics data.';
const [selectedStage] = stages;
const selectedStageSlug = selectedStage.slug;
const [selectedValueStream] = valueStreams;
const stageEndpoint = ({ stageId }) =>
`/groups/${selectedGroup.fullPath}/-/analytics/value_stream_analytics/stages/${stageId}`;
......@@ -54,10 +56,11 @@ describe('Cycle analytics actions', () => {
});
it.each`
action | type | stateKey | payload
${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }}
action | type | stateKey | payload
${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'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 }) => {
return testAction(
actions[action],
......@@ -133,7 +136,7 @@ describe('Cycle analytics actions', () => {
beforeEach(() => {
state = { ...state, selectedGroup };
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageData).reply(200, { events: [] });
mock.onGet(endpoints.stageData).reply(httpStatusCodes.OK, { events: [] });
});
it('dispatches receiveStageDataSuccess with received data on success', () => {
......@@ -246,6 +249,7 @@ describe('Cycle analytics actions', () => {
[],
[
{ type: 'requestCycleAnalyticsData' },
{ type: 'fetchValueStreams' },
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'fetchStageMedianValues' },
{ type: 'receiveCycleAnalyticsDataSuccess' },
......@@ -416,7 +420,7 @@ describe('Cycle analytics actions', () => {
const payload = { hidden: true };
beforeEach(() => {
mock.onPut(stageEndpoint({ stageId }), payload).replyOnce(200, payload);
mock.onPut(stageEndpoint({ stageId }), payload).replyOnce(httpStatusCodes.OK, payload);
state = { selectedGroup };
});
......@@ -562,7 +566,7 @@ describe('Cycle analytics actions', () => {
const stageId = 'cool-stage';
beforeEach(() => {
mock.onDelete(stageEndpoint({ stageId })).replyOnce(200);
mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.OK);
state = { selectedGroup };
});
......@@ -614,7 +618,7 @@ describe('Cycle analytics actions', () => {
const stageId = 'cool-stage';
beforeEach(() => {
mock.onDelete(stageEndpoint({ stageId })).replyOnce(200);
mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.OK);
state = { selectedGroup };
});
......@@ -647,7 +651,7 @@ describe('Cycle analytics actions', () => {
beforeEach(() => {
state = { ...state, stages: [{ slug: selectedStageSlug }], selectedGroup };
mock = new MockAdapter(axios);
mock.onGet(endpoints.stageMedian).reply(200, { events: [] });
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.OK, { events: [] });
mockDispatch = jest.fn();
});
......@@ -875,13 +879,11 @@ describe('Cycle analytics actions', () => {
payload,
state,
[
{ type: types.REQUEST_CREATE_VALUE_STREAM },
{
type: types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS,
payload: { status: httpStatusCodes.OK, data: {} },
type: types.REQUEST_CREATE_VALUE_STREAM,
},
],
[],
[{ type: 'receiveCreateValueStreamSuccess' }],
);
});
});
......@@ -909,4 +911,92 @@ describe('Cycle analytics actions', () => {
});
});
});
describe('fetchValueStreams', () => {
beforeEach(() => {
state = {
...state,
stages: [{ slug: selectedStageSlug }],
selectedGroup,
featureFlags: {
...state.featureFlags,
hasCreateMultipleValueStreams: true,
},
};
mock = new MockAdapter(axios);
mock.onGet(endpoints.valueStreamData).reply(httpStatusCodes.OK, { stages: [], events: [] });
});
it(`commits ${types.REQUEST_VALUE_STREAMS} and dispatches receiveValueStreamsSuccess with received data on success`, () => {
return testAction(
actions.fetchValueStreams,
null,
state,
[{ type: types.REQUEST_VALUE_STREAMS }],
[
{
payload: {
events: [],
stages: [],
},
type: 'receiveValueStreamsSuccess',
},
],
);
});
describe('with a failing request', () => {
const resp = { data: {} };
beforeEach(() => {
mock.onGet(endpoints.valueStreamData).reply(httpStatusCodes.NOT_FOUND, resp);
});
it(`will commit ${types.RECEIVE_VALUE_STREAMS_ERROR}`, () => {
return testAction(
actions.fetchValueStreams,
null,
state,
[
{ type: types.REQUEST_VALUE_STREAMS },
{
type: types.RECEIVE_VALUE_STREAMS_ERROR,
},
],
[],
);
});
});
describe('receiveValueStreamsSuccess', () => {
it(`commits the ${types.RECEIVE_VALUE_STREAMS_SUCCESS} mutation`, () => {
return testAction(
actions.receiveValueStreamsSuccess,
valueStreams,
state,
[
{
type: types.RECEIVE_VALUE_STREAMS_SUCCESS,
payload: valueStreams,
},
],
[{ type: 'setSelectedValueStream', payload: selectedValueStream.id }],
);
});
});
describe('with hasCreateMultipleValueStreams disabled', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasCreateMultipleValueStreams: false,
},
};
});
it(`will skip making a request`, () =>
testAction(actions.fetchValueStreams, null, state, [], []));
});
});
});
......@@ -12,6 +12,7 @@ import {
endDate,
selectedProjects,
customizableStagesAndEvents,
valueStreams,
} from '../mock_data';
let state = null;
......@@ -27,6 +28,10 @@ describe('Cycle analytics mutations', () => {
it.each`
mutation | stateKey | value
${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]}
${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]}
${types.REQUEST_VALUE_STREAMS} | ${'isLoadingValueStreams'} | ${true}
${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'isLoadingValueStreams'} | ${false}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
......@@ -59,18 +64,33 @@ describe('Cycle analytics mutations', () => {
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${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_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }}
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
state = {
selectedGroup: { fullPath: 'rad-stage' },
};
state = { selectedGroup: { fullPath: 'rad-stage' } };
mutations[mutation](state, payload);
expect(state).toMatchObject(expectedState);
},
);
describe('with value streams available', () => {
it.each`
mutation | payload | expectedState
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: valueStreams[1] }}
${types.SET_SELECTED_VALUE_STREAM} | ${'fake-id'} | ${{ selectedValueStream: {} }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
state = { valueStreams };
mutations[mutation](state, payload);
expect(state).toMatchObject(expectedState);
},
);
});
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false and errorCode=null', () => {
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
......
......@@ -334,6 +334,45 @@ describe('Api', () => {
});
});
describe('cycleAnalyticsValueStreams', () => {
it('fetches custom value streams', done => {
const response = [{ name: 'value stream 1', id: 1 }];
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/value_streams`;
mock.onGet(expectedUrl).reply(httpStatus.OK, response);
Api.cycleAnalyticsValueStreams(groupId)
.then(responseObj =>
expectRequestWithCorrectParameters(responseObj, {
response,
expectedUrl,
}),
)
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsCreateValueStream', () => {
it('submit the custom value stream data', done => {
const response = {};
const customValueStream = {
name: 'cool-value-stream-stage',
};
const expectedUrl = `${dummyCycleAnalyticsUrlRoot}/-/analytics/value_stream_analytics/value_streams`;
mock.onPost(expectedUrl).reply(httpStatus.OK, response);
Api.cycleAnalyticsCreateValueStream(groupId, customValueStream)
.then(({ data, config: { data: reqData, url } }) => {
expect(data).toEqual(response);
expect(JSON.parse(reqData)).toMatchObject(customValueStream);
expect(url).toEqual(expectedUrl);
})
.then(done)
.catch(done.fail);
});
});
describe('cycleAnalyticsGroupStagesAndEvents', () => {
it('fetches custom stage events and all stages', done => {
const response = { events: [], stages: [] };
......
......@@ -28,6 +28,10 @@ RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
expect(stages.map(&:id)).to all(be_nil)
end
it 'does not persist the value stream record' do
expect { subject }.not_to change { Analytics::CycleAnalytics::GroupValueStream.count }
end
context 'when there are persisted stages' do
let_it_be(:stage1) { create(:cycle_analytics_group_stage, parent: group, relative_position: 2, value_stream: value_stream) }
let_it_be(:stage2) { create(:cycle_analytics_group_stage, parent: group, relative_position: 3, value_stream: value_stream) }
......
......@@ -6860,6 +6860,9 @@ msgstr ""
msgid "Create Project"
msgstr ""
msgid "Create Value Stream"
msgstr ""
msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
......@@ -6956,6 +6959,9 @@ msgstr ""
msgid "Create new"
msgstr ""
msgid "Create new Value Stream"
msgstr ""
msgid "Create new board"
msgstr ""
......@@ -6977,9 +6983,6 @@ msgstr ""
msgid "Create new label"
msgstr ""
msgid "Create new value stream"
msgstr ""
msgid "Create new..."
msgstr ""
......@@ -6995,9 +6998,6 @@ msgstr ""
msgid "Create snippet"
msgstr ""
msgid "Create value stream"
msgstr ""
msgid "Create wildcard: %{searchTerm}"
msgstr ""
......@@ -9709,7 +9709,7 @@ msgstr ""
msgid "Example: @sub\\.company\\.com$"
msgstr ""
msgid "Example: My value stream"
msgid "Example: My Value Stream"
msgstr ""
msgid "Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula."
......
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