Commit b4108e90 authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'ek-use-be-default-value-stream-stages' into 'master'

Expose default value stream configuration

See merge request gitlab-org/gitlab!53688
parents 42d0e21e 8aadce6f
# frozen_string_literal: true
module Analytics
module CycleAnalyticsHelper
def cycle_analytics_default_stage_config
Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params|
Analytics::CycleAnalytics::StagePresenter.new(stage_params)
end
end
end
end
import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const NAME_MAX_LENGTH = 100;
......@@ -32,6 +31,9 @@ export const I18N = {
HIDDEN_DEFAULT_STAGE: s__('CreateValueStreamForm|%{name} (default)'),
TEMPLATE_DEFAULT: s__('CreateValueStreamForm|Create from default template'),
TEMPLATE_BLANK: s__('CreateValueStreamForm|Create from no template'),
ISSUE_STAGE_END: s__('CreateValueStreamForm|Issue stage end'),
PLAN_STAGE_START: s__('CreateValueStreamForm|Plan stage start'),
CODE_STAGE_START: s__('CreateValueStreamForm|Code stage start'),
};
export const ERRORS = {
......@@ -73,51 +75,6 @@ export const defaultFields = {
export const defaultCustomStageFields = { ...defaultFields, custom: true };
/**
* These stage configs are copied from the https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/cycle_analytics
* This is a stopgap solution and we should eventually provide these from an API endpoint
*
* More information: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49094#note_464116439
*/
const BASE_DEFAULT_STAGE_CONFIG = [
{
id: 'issue',
startEventIdentifier: ['issue_created'],
endEventIdentifier: ['issue_first_associated_with_milestone', 'issue_first_added_to_board'],
},
{
id: 'plan',
startEventIdentifier: ['issue_first_associated_with_milestone', 'issue_first_added_to_board'],
endEventIdentifier: ['issue_first_mentioned_in_commit'],
},
{
id: 'code',
startEventIdentifier: ['issue_first_mentioned_in_commit'],
endEventIdentifier: ['merge_request_created'],
},
{
id: 'test',
startEventIdentifier: ['merge_request_last_build_started'],
endEventIdentifier: ['merge_request_last_build_finished'],
},
{
id: 'review',
startEventIdentifier: ['merge_request_created'],
endEventIdentifier: ['merge_request_merged'],
},
{
id: 'staging',
startEventIdentifier: ['merge_request_merged'],
endEventIdentifier: ['merge_request_first_deployed_to_production'],
},
];
export const DEFAULT_STAGE_CONFIG = BASE_DEFAULT_STAGE_CONFIG.map(({ id, ...rest }) => ({
...rest,
custom: false,
name: capitalizeFirstCharacter(id),
}));
export const PRESET_OPTIONS_DEFAULT = 'default';
export const PRESET_OPTIONS_BLANK = 'blank';
export const PRESET_OPTIONS = [
......@@ -130,3 +87,22 @@ export const PRESET_OPTIONS = [
value: PRESET_OPTIONS_BLANK,
},
];
// These events can only be set on the back end, they are used in the
// initial configuration of some default stages, but should not be
// selectable by users via the form, they are added here only for display
// purposes when we are editing a default value stream
export const ADDITIONAL_DEFAULT_STAGE_EVENTS = [
{
identifier: 'issue_stage_end',
name: I18N.ISSUE_STAGE_END,
},
{
identifier: 'plan_stage_start',
name: I18N.PLAN_STAGE_START,
},
{
identifier: 'code_stage_start',
name: I18N.CODE_STAGE_START,
},
];
<script>
import { GlFormGroup, GlFormInput, GlFormText } from '@gitlab/ui';
import StageFieldActions from './stage_field_actions.vue';
import { I18N } from './constants';
import { I18N, ADDITIONAL_DEFAULT_STAGE_EVENTS } from './constants';
const findStageEvent = (stageEvents = [], eid = null) => {
if (!eid) return '';
return stageEvents.find(({ identifier }) => identifier === eid);
};
const eventIdsToName = (stageEvents = [], eventIds = []) =>
eventIds
.map((eid) => {
const stage = findStageEvent(stageEvents, eid);
return stage?.name || '';
})
.join(', ');
const eventIdToName = (stageEvents = [], eid) => {
const event = findStageEvent(stageEvents, eid);
return event?.name || '';
};
export default {
name: 'DefaultStageFields',
......@@ -54,8 +51,8 @@ export default {
renderError(field) {
return this.errors[field] ? this.errors[field]?.join('\n') : null;
},
eventName(eventIds = []) {
return eventIdsToName(this.stageEvents, eventIds);
eventName(eventId) {
return eventIdToName([...this.stageEvents, ...ADDITIONAL_DEFAULT_STAGE_EVENTS], eventId);
},
},
I18N,
......
......@@ -6,7 +6,6 @@ import { sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { swapArrayItems } from '~/lib/utils/array_utility';
import {
DEFAULT_STAGE_CONFIG,
STAGE_SORT_DIRECTION,
I18N,
defaultCustomStageFields,
......@@ -17,12 +16,12 @@ import { validateValueStreamName, validateStage } from './create_value_stream_fo
import DefaultStageFields from './create_value_stream_form/default_stage_fields.vue';
import CustomStageFields from './create_value_stream_form/custom_stage_fields.vue';
const initializeStageErrors = (selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT ? DEFAULT_STAGE_CONFIG.map(() => ({})) : [{}];
const initializeStageErrors = (defaultStageConfig, selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT ? defaultStageConfig.map(() => ({})) : [{}];
const initializeStages = (selectedPreset = PRESET_OPTIONS_DEFAULT) =>
const initializeStages = (defaultStageConfig, selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT
? DEFAULT_STAGE_CONFIG
? defaultStageConfig
: [{ ...defaultCustomStageFields }];
const formatStageDataForSubmission = (stages) => {
......@@ -68,14 +67,24 @@ export default {
required: false,
default: false,
},
defaultStageConfig: {
type: Array,
required: true,
},
},
data() {
const { hasExtendedFormFields, initialData, initialFormErrors, initialPreset } = this;
const {
defaultStageConfig = [],
hasExtendedFormFields,
initialData,
initialFormErrors,
initialPreset,
} = this;
const { name: nameError = [], stages: stageErrors = [{}] } = initialFormErrors;
const additionalFields = hasExtendedFormFields
? {
stages: initializeStages(initialPreset),
stageErrors: stageErrors || initializeStageErrors(initialPreset),
stages: initializeStages(defaultStageConfig, initialPreset),
stageErrors: stageErrors || initializeStageErrors(defaultStageConfig, initialPreset),
...initialData,
}
: { stages: [], nameError };
......@@ -91,9 +100,7 @@ export default {
};
},
computed: {
...mapState({
isCreating: 'isCreatingValueStream',
}),
...mapState({ isCreating: 'isCreatingValueStream' }),
...mapState('customStages', ['formEvents']),
isValueStreamNameValid() {
return !this.nameError?.length;
......@@ -151,8 +158,8 @@ export default {
});
this.name = '';
this.nameError = [];
this.stages = initializeStages(this.selectedPreset);
this.stageErrors = initializeStageErrors(this.selectedPreset);
this.stages = initializeStages(this.defaultStageConfig, this.selectedPreset);
this.stageErrors = initializeStageErrors(this.defaultStageConfig, this.selectedPreset);
}
});
},
......@@ -222,7 +229,7 @@ export default {
},
handleResetDefaults() {
this.name = '';
DEFAULT_STAGE_CONFIG.forEach((stage, index) => {
this.defaultStageConfig.forEach((stage, index) => {
Vue.set(this.stages, index, { ...stage, hidden: false });
});
},
......@@ -236,7 +243,11 @@ export default {
} else {
this.handleResetBlank();
}
Vue.set(this, 'stageErrors', initializeStageErrors(this.selectedPreset));
Vue.set(
this,
'stageErrors',
initializeStageErrors(this.defaultStageConfig, this.selectedPreset),
);
},
},
I18N,
......
......@@ -48,6 +48,7 @@ export default {
data: 'valueStreams',
selectedValueStream: 'selectedValueStream',
initialFormErrors: 'createValueStreamErrors',
defaultStageConfig: 'defaultStageConfig',
}),
hasValueStreams() {
return Boolean(this.data.length);
......@@ -127,6 +128,7 @@ export default {
<value-stream-form
:initial-form-errors="initialFormErrors"
:has-extended-form-fields="hasExtendedFormFields"
:default-stage-config="defaultStageConfig"
/>
<gl-modal
data-testid="delete-value-stream-modal"
......
......@@ -93,6 +93,7 @@ export default {
createdBefore: endDate = null,
selectedProjects = [],
selectedValueStream = {},
defaultStageConfig = [],
} = {},
) {
state.isLoading = true;
......@@ -101,6 +102,7 @@ export default {
state.selectedValueStream = selectedValueStream;
state.startDate = startDate;
state.endDate = endDate;
state.defaultStageConfig = defaultStageConfig;
},
[types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS](state) {
state.isLoading = false;
......
export default () => ({
featureFlags: {},
defaultStageConfig: [],
startDate: null,
endDate: null,
......
......@@ -21,6 +21,17 @@ export const buildValueStreamFromJson = (valueStream) => {
return id ? { id, name, isCustom } : null;
};
/**
* Creates an array of stage objects from a json string. Returns an empty array if no stages are present.
*
* @param {String} stages - JSON encoded array of stages
* @returns {Array} - An array of stage objects
*/
const buildDefaultStagesFromJSON = (stages = '') => {
if (!stages.length) return [];
return JSON.parse(stages);
};
/**
* Creates a group object from a dataset. Returns null if no groupId is present.
*
......@@ -70,7 +81,7 @@ export const buildProjectFromDataset = (dataset) => {
* @param {String} data - JSON encoded array of projects
* @returns {Array} - An array of project objects
*/
const buildProjectsFromJSON = (projects = []) => {
const buildProjectsFromJSON = (projects = '') => {
if (!projects.length) return [];
return JSON.parse(projects);
};
......@@ -93,6 +104,7 @@ export const buildCycleAnalyticsInitialData = ({
groupAvatarUrl = null,
labelsPath = '',
milestonesPath = '',
defaultStages = null,
} = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId
......@@ -113,6 +125,9 @@ export const buildCycleAnalyticsInitialData = ({
: [],
labelsPath,
milestonesPath,
defaultStageConfig: defaultStages
? buildDefaultStagesFromJSON(defaultStages).map(convertObjectPropsToCamelCase)
: [],
});
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
......
......@@ -2,7 +2,8 @@
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {}
- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group) } : {}
- image_paths = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg")}
- data_attributes.merge!(api_paths, image_paths)
- default_stages = { default_stages: cycle_analytics_default_stage_config.to_json }
- data_attributes.merge!(api_paths, image_paths, default_stages)
- add_page_specific_style 'page_bundles/cycle_analytics'
#js-cycle-analytics-app{ data: data_attributes }
......@@ -14,8 +14,8 @@ const ISSUE_CREATED = { id: 'issue_created', name: 'Issue created' };
const ISSUE_CLOSED = { id: 'issue_closed', name: 'Issue closed' };
const defaultStage = {
name: 'Cool new stage',
startEventIdentifier: [ISSUE_CREATED.id],
endEventIdentifier: [ISSUE_CLOSED.id],
startEventIdentifier: ISSUE_CREATED.id,
endEventIdentifier: ISSUE_CLOSED.id,
endEventLabel: 'some_label',
};
......
......@@ -6,7 +6,7 @@ import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_v
import CustomStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/custom_stage_fields.vue';
import { PRESET_OPTIONS_BLANK } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { customStageEvents as formEvents } from '../mock_data';
import { customStageEvents as formEvents, defaultStageConfig } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -28,11 +28,10 @@ describe('ValueStreamForm', () => {
],
};
const fakeStore = ({ initialState = {} }) =>
const fakeStore = () =>
new Vuex.Store({
state: {
isCreatingValueStream: false,
...initialState,
},
actions: {
createValueStream: createValueStreamMock,
......@@ -47,17 +46,18 @@ describe('ValueStreamForm', () => {
},
});
const createComponent = ({ props = {}, data = {}, initialState = {}, stubs = {} } = {}) =>
const createComponent = ({ props = {}, data = {}, stubs = {} } = {}) =>
extendedWrapper(
shallowMount(ValueStreamForm, {
localVue,
store: fakeStore({ initialState }),
store: fakeStore(),
data() {
return {
...data,
};
},
propsData: {
defaultStageConfig,
...props,
},
mocks: {
......@@ -132,11 +132,11 @@ describe('ValueStreamForm', () => {
});
it('adds a blank custom stage when clicked', () => {
expect(wrapper.vm.stages.length).toBe(6);
expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length);
clickAddStage();
expect(wrapper.vm.stages.length).toBe(7);
expect(wrapper.vm.stages.length).toBe(defaultStageConfig.length + 1);
});
it('validates existing fields when clicked', () => {
......
......@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import { findDropdownItemText } from '../helpers';
import { valueStreams } from '../mock_data';
import { valueStreams, defaultStageConfig } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -28,6 +28,7 @@ describe('ValueStreamSelect', () => {
deleteValueStreamError: null,
valueStreams: [],
selectedValueStream: {},
defaultStageConfig,
...initialState,
},
actions: {
......
......@@ -69,6 +69,30 @@ export const customizableStagesAndEvents = getJSONFixture(
const dummyState = {};
export const defaultStageConfig = [
{
name: 'issue',
custom: false,
relativePosition: 1,
startEventIdentifier: 'issue_created',
endEventIdentifier: 'issue_stage_end',
},
{
name: 'plan',
custom: false,
relativePosition: 2,
startEventIdentifier: 'plan_stage_start',
endEventIdentifier: 'issue_first_mentioned_in_commit',
},
{
name: 'code',
custom: false,
relativePosition: 3,
startEventIdentifier: 'code_stage_start',
endEventIdentifier: 'merge_request_created',
},
];
// prepare the raw stage data for our components
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](dummyState, customizableStagesAndEvents.stages);
......
......@@ -8562,6 +8562,9 @@ msgstr ""
msgid "CreateValueStreamForm|All default stages are currently visible"
msgstr ""
msgid "CreateValueStreamForm|Code stage start"
msgstr ""
msgid "CreateValueStreamForm|Create from default template"
msgstr ""
......@@ -8589,6 +8592,9 @@ msgstr ""
msgid "CreateValueStreamForm|Enter value stream name"
msgstr ""
msgid "CreateValueStreamForm|Issue stage end"
msgstr ""
msgid "CreateValueStreamForm|Maximum length %{maxLength} characters"
msgstr ""
......@@ -8598,6 +8604,9 @@ msgstr ""
msgid "CreateValueStreamForm|New stage"
msgstr ""
msgid "CreateValueStreamForm|Plan stage start"
msgstr ""
msgid "CreateValueStreamForm|Please select a start event first"
msgstr ""
......
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