Commit 8aadce6f authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Luke Duncalfe

Provide default value stream configs

Provides the default values stream stage configs
from the backend via data attributes
parent 3b400367
# 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