Commit 0b5c8759 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '32019-follow-up-add-vuex-to-customizable-cycle-analytics' into 'master'

Resolve "Follow up - Add Vuex to customizable cycle analytics"

Closes #32019

See merge request gitlab-org/gitlab!17208
parents 974fa05f fe561d59
export default {
data() {
return {
isCustomStageForm: false,
};
},
methods: {
showAddStageForm: () => {},
hideAddStageForm: () => {},
},
};
...@@ -3,7 +3,6 @@ import Vue from 'vue'; ...@@ -3,7 +3,6 @@ import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins'; import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
import addStageMixin from 'ee_else_ce/analytics/cycle_analytics/mixins/add_stage_mixin';
import Flash from '../flash'; import Flash from '../flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
...@@ -44,14 +43,8 @@ export default () => { ...@@ -44,14 +43,8 @@ export default () => {
DateRangeDropdown: () => DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'), import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
'stage-nav-item': stageNavItem, 'stage-nav-item': stageNavItem,
CustomStageForm: () =>
import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
AddStageButton: () =>
import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'),
CustomStageFormContainer: () =>
import('ee_component/analytics/cycle_analytics/components/custom_stage_form_container.vue'),
}, },
mixins: [filterMixins, addStageMixin], mixins: [filterMixins],
data() { data() {
return { return {
store: CycleAnalyticsStore, store: CycleAnalyticsStore,
...@@ -131,7 +124,6 @@ export default () => { ...@@ -131,7 +124,6 @@ export default () => {
return; return;
} }
this.hideAddStageForm();
this.isLoadingStage = true; this.isLoadingStage = true;
this.store.setStageEvents([], stage); this.store.setStageEvents([], stage);
this.store.setActiveStage(stage); this.store.setActiveStage(stage);
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue'; import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
import DateRangeDropdown from '../../shared/components/date_range_dropdown.vue'; import DateRangeDropdown from '../../shared/components/date_range_dropdown.vue';
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
SummaryTable, SummaryTable,
StageTable, StageTable,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
emptyStateSvgPath: { emptyStateSvgPath: {
type: String, type: String,
...@@ -45,35 +47,38 @@ export default { ...@@ -45,35 +47,38 @@ export default {
...mapState([ ...mapState([
'isLoading', 'isLoading',
'isLoadingStage', 'isLoadingStage',
'isLoadingStageForm',
'isEmptyStage', 'isEmptyStage',
'isAddingCustomStage', 'isAddingCustomStage',
'selectedGroup', 'selectedGroup',
'selectedProjectIds', 'selectedProjectIds',
'selectedStageName', 'selectedStageName',
'events',
'stages', 'stages',
'summary', 'summary',
'dataTimeframe', 'dataTimeframe',
'labels',
'currentStageEvents',
'customStageFormEvents',
]), ]),
...mapGetters(['currentStage', 'defaultStage', 'hasNoAccessError', 'currentGroupPath']), ...mapGetters(['currentStage', 'defaultStage', 'hasNoAccessError', 'currentGroupPath']),
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.selectedGroup; return !this.selectedGroup;
}, },
hasCustomizableCycleAnalytics() { hasCustomizableCycleAnalytics() {
return gon && gon.features ? gon.features.customizableCycleAnalytics : false; return Boolean(this.glFeatures.customizableCycleAnalytics);
}, },
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'fetchCustomStageFormData',
'fetchCycleAnalyticsData',
'fetchStageData',
'setCycleAnalyticsDataEndpoint', 'setCycleAnalyticsDataEndpoint',
'setStageDataEndpoint', 'setStageDataEndpoint',
'setSelectedGroup', 'setSelectedGroup',
'fetchCycleAnalyticsData',
'setSelectedProjects', 'setSelectedProjects',
'setSelectedTimeframe', 'setSelectedTimeframe',
'fetchStageData',
'setSelectedStageName', 'setSelectedStageName',
'showCustomStageForm',
'hideCustomStageForm', 'hideCustomStageForm',
]), ]),
onGroupSelect(group) { onGroupSelect(group) {
...@@ -97,7 +102,7 @@ export default { ...@@ -97,7 +102,7 @@ export default {
this.fetchStageData(this.currentStage.name); this.fetchStageData(this.currentStage.name);
}, },
onShowAddStageForm() { onShowAddStageForm() {
this.showCustomStageForm(); this.fetchCustomStageFormData(this.currentGroupPath);
}, },
}, },
}; };
...@@ -161,22 +166,26 @@ export default { ...@@ -161,22 +166,26 @@ export default {
) )
" "
/> />
<summary-table class="js-summary-table" :items="summary" /> <div v-else>
<stage-table <summary-table class="js-summary-table" :items="summary" />
v-if="currentStage" <stage-table
class="js-stage-table" v-if="currentStage"
:current-stage="currentStage" class="js-stage-table"
:stages="stages" :current-stage="currentStage"
:is-loading-stage="isLoadingStage" :stages="stages"
:is-empty-stage="isEmptyStage" :is-loading="isLoadingStage || isLoadingStageForm"
:is-adding-custom-stage="isAddingCustomStage" :is-empty-stage="isEmptyStage"
:events="events" :is-adding-custom-stage="isAddingCustomStage"
:no-data-svg-path="noDataSvgPath" :current-stage-events="currentStageEvents"
:no-access-svg-path="noAccessSvgPath" :custom-stage-form-events="customStageFormEvents"
:can-edit-stages="hasCustomizableCycleAnalytics" :labels="labels"
@selectStage="onStageSelect" :no-data-svg-path="noDataSvgPath"
@showAddStageForm="onShowAddStageForm" :no-access-svg-path="noAccessSvgPath"
/> :can-edit-stages="hasCustomizableCycleAnalytics"
@selectStage="onStageSelect"
@showAddStageForm="onShowAddStageForm"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
import { isEqual } from 'underscore'; import { isEqual } from 'underscore';
import { GlButton, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui'; import { GlButton, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import LabelsSelector from './labels_selector.vue'; import LabelsSelector from './labels_selector.vue';
import { import {
isStartEvent, isStartEvent,
isLabelEvent, isLabelEvent,
......
<script>
// NOTE: this is a temporary component while cycle-analytics is being refactored
// post refactor we will have a vuex store and functionality to fetch data
// https://gitlab.com/gitlab-org/gitlab/issues/32019
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import Api from '~/api';
import CustomStageForm from './custom_stage_form.vue';
export default {
name: 'CustomStageFormContainer',
components: {
CustomStageForm,
GlLoadingIcon,
},
props: {
namespace: {
type: String,
required: true,
},
},
data() {
return {
// NOTE: events will be part of the response from the new cycle analytics backend
// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31535
events: [],
labels: [],
isLoading: false,
};
},
created() {
this.isLoading = true;
Api.groupLabels(this.namespace)
.then(labels => {
this.labels = labels.map(({ title, ...rest }) => ({ ...rest, name: title }));
})
.catch(() => {
createFlash(__('There was an error fetching the form data'));
})
.finally(() => {
this.isLoading = false;
});
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="md" class="my-3" />
<custom-stage-form v-else :labels="labels" :events="events" />
</template>
...@@ -19,13 +19,11 @@ export default { ...@@ -19,13 +19,11 @@ export default {
required: false, required: false,
}, },
}, },
computed: { data() {
activeClass() { return {
return 'active font-weight-bold border-style-solid border-color-blue-300'; activeClass: 'active font-weight-bold border-color-blue-300',
}, inactiveClass: 'bg-transparent border-color-default',
inactiveClass() { };
return 'bg-transparent border-style-dashed border-color-default';
},
}, },
}; };
</script> </script>
...@@ -33,7 +31,7 @@ export default { ...@@ -33,7 +31,7 @@ export default {
<template> <template>
<div <div
:class="[isActive ? activeClass : inactiveClass]" :class="[isActive ? activeClass : inactiveClass]"
class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-width-1px" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-width-1px border-style-solid"
> >
<slot></slot> <slot></slot>
<div v-if="canEdit" class="dropdown"> <div v-if="canEdit" class="dropdown">
......
...@@ -6,7 +6,7 @@ import StageNavItem from './stage_nav_item.vue'; ...@@ -6,7 +6,7 @@ import StageNavItem from './stage_nav_item.vue';
import StageEventList from './stage_event_list.vue'; import StageEventList from './stage_event_list.vue';
import StageTableHeader from './stage_table_header.vue'; import StageTableHeader from './stage_table_header.vue';
import AddStageButton from './add_stage_button.vue'; import AddStageButton from './add_stage_button.vue';
import CustomStageFormContainer from './custom_stage_form_container.vue'; import CustomStageForm from './custom_stage_form.vue';
export default { export default {
name: 'StageTable', name: 'StageTable',
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
StageNavItem, StageNavItem,
StageTableHeader, StageTableHeader,
AddStageButton, AddStageButton,
CustomStageFormContainer, CustomStageForm,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isLoadingStage: { isLoading: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
...@@ -44,7 +44,15 @@ export default { ...@@ -44,7 +44,15 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
events: { currentStageEvents: {
type: Array,
required: true,
},
customStageFormEvents: {
type: Array,
required: true,
},
labels: {
type: Array, type: Array,
required: true, required: true,
}, },
...@@ -60,19 +68,14 @@ export default { ...@@ -60,19 +68,14 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
groupPath: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
stageName() { stageName() {
return this.currentStage ? this.currentStage.legend : __('Related Issues'); return this.currentStage ? this.currentStage.legend : __('Related Issues');
}, },
shouldDisplayStage() { shouldDisplayStage() {
const { events = [], isLoadingStage, isEmptyStage } = this; const { currentStageEvents = [], isLoading, isEmptyStage } = this;
return events.length && !isLoadingStage && !isEmptyStage; return currentStageEvents.length && !isLoading && !isEmptyStage;
}, },
stageHeaders() { stageHeaders() {
return [ return [
...@@ -147,20 +150,24 @@ export default { ...@@ -147,20 +150,24 @@ export default {
</ul> </ul>
</nav> </nav>
<div class="section stage-events"> <div class="section stage-events">
<gl-loading-icon v-if="isLoadingStage" class="mt-4" size="md" /> <gl-loading-icon v-if="isLoading" class="mt-4" size="md" />
<gl-empty-state <gl-empty-state
v-else-if="currentStage && !currentStage.isUserAllowed" v-else-if="currentStage && !currentStage.isUserAllowed"
:title="__('You need permission.')" :title="__('You need permission.')"
:description="__('Want to see the data? Please ask an administrator for access.')" :description="__('Want to see the data? Please ask an administrator for access.')"
:svg-path="noAccessSvgPath" :svg-path="noAccessSvgPath"
/> />
<custom-stage-form-container <custom-stage-form
v-else-if="isAddingCustomStage" v-else-if="isAddingCustomStage"
:events="events" :events="customStageFormEvents"
:namespace="groupPath" :labels="labels"
/> />
<template v-else> <template v-else>
<stage-event-list v-if="shouldDisplayStage" :stage="currentStage" :events="events" /> <stage-event-list
v-if="shouldDisplayStage"
:stage="currentStage"
:events="currentStageEvents"
/>
<gl-empty-state <gl-empty-state
v-if="isEmptyStage" v-if="isEmptyStage"
:title="__('We don\'t have enough data to show this stage.')" :title="__('We don\'t have enough data to show this stage.')"
......
export default {
data() {
return {
isCustomStageForm: false,
};
},
methods: {
showAddStageForm() {
if (this.store) {
this.store.deactivateAllStages();
}
this.isCustomStageForm = true;
},
hideAddStageForm() {
this.isCustomStageForm = false;
},
},
};
...@@ -2,9 +2,11 @@ import * as types from './mutation_types'; ...@@ -2,9 +2,11 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Api from '~/api';
export const setCycleAnalyticsDataEndpoint = ({ commit }, groupPath) => export const setCycleAnalyticsDataEndpoint = ({ commit }, groupPath) =>
commit(types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT, groupPath); commit(types.SET_CYCLE_ANALYTICS_DATA_ENDPOINT, groupPath);
export const setStageDataEndpoint = ({ commit }, stageSlug) => export const setStageDataEndpoint = ({ commit }, stageSlug) =>
commit(types.SET_STAGE_DATA_ENDPOINT, stageSlug); commit(types.SET_STAGE_DATA_ENDPOINT, stageSlug);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group); export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
...@@ -50,6 +52,7 @@ export const receiveCycleAnalyticsDataSuccess = ({ state, commit, dispatch }, da ...@@ -50,6 +52,7 @@ export const receiveCycleAnalyticsDataSuccess = ({ state, commit, dispatch }, da
createFlash(__('There was an error while fetching cycle analytics data.')); createFlash(__('There was an error while fetching cycle analytics data.'));
} }
}; };
export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => { export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => {
const { status } = response; const { status } = response;
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR, status); commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR, status);
...@@ -70,10 +73,21 @@ export const fetchCycleAnalyticsData = ({ state, dispatch }) => { ...@@ -70,10 +73,21 @@ export const fetchCycleAnalyticsData = ({ state, dispatch }) => {
.catch(error => dispatch('receiveCycleAnalyticsDataError', error)); .catch(error => dispatch('receiveCycleAnalyticsDataError', error));
}; };
export const showCustomStageForm = ({ commit }) => { export const hideCustomStageForm = ({ commit }) => commit(types.HIDE_CUSTOM_STAGE_FORM);
commit(types.SHOW_CUSTOM_STAGE_FORM);
export const receiveCustomStageFormDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS, data);
export const receiveCustomStageFormDataError = ({ commit }, error) => {
commit(types.RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR, error);
createFlash(__('There was an error fetching data for the form'));
}; };
export const requestCustomStageFormData = ({ commit }) =>
commit(types.REQUEST_CUSTOM_STAGE_FORM_DATA);
export const fetchCustomStageFormData = ({ dispatch }, groupPath) => {
dispatch('requestCustomStageFormData');
export const hideCustomStageForm = ({ commit }) => { return Api.groupLabels(groupPath)
commit(types.HIDE_CUSTOM_STAGE_FORM); .then(data => dispatch('receiveCustomStageFormDataSuccess', data))
.catch(error => dispatch('receiveCustomStageFormDataError', error));
}; };
...@@ -9,4 +9,4 @@ export const defaultStage = state => (state.stages.length ? state.stages[0] : nu ...@@ -9,4 +9,4 @@ export const defaultStage = state => (state.stages.length ? state.stages[0] : nu
export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN; export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDEN;
export const currentGroupPath = state => export const currentGroupPath = state =>
state.selectedGroup ? state.selectedGroup.full_path : null; state.selectedGroup && state.selectedGroup.full_path ? state.selectedGroup.full_path : null;
...@@ -14,5 +14,8 @@ export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA'; ...@@ -14,5 +14,8 @@ export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS'; export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR'; export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM';
export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM'; export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM';
export const REQUEST_CUSTOM_STAGE_FORM_DATA = 'REQUEST_CUSTOM_STAGE_FORM_DATA';
export const RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS = 'RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS';
export const RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR = 'RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR';
...@@ -58,25 +58,32 @@ export default { ...@@ -58,25 +58,32 @@ export default {
[types.REQUEST_STAGE_DATA](state) { [types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true; state.isLoadingStage = true;
}, },
[types.RECEIVE_STAGE_DATA_SUCCESS](state, data) { [types.RECEIVE_STAGE_DATA_SUCCESS](state, data = {}) {
state.events = data.events.map(({ name = '', ...rest }) => const { events = [] } = data;
state.currentStageEvents = events.map(({ name = '', ...rest }) =>
convertObjectPropsToCamelCase({ title: name, ...rest }, { deep: true }), convertObjectPropsToCamelCase({ title: name, ...rest }, { deep: true }),
); );
state.isEmptyStage = state.events.length === 0; state.isEmptyStage = state.currentStageEvents.length === 0;
state.isLoadingStage = false; state.isLoadingStage = false;
}, },
[types.RECEIVE_STAGE_DATA_ERROR](state) { [types.RECEIVE_STAGE_DATA_ERROR](state) {
state.isEmptyStage = true; state.isEmptyStage = true;
state.isLoadingStage = false; state.isLoadingStage = false;
}, },
[types.SHOW_CUSTOM_STAGE_FORM](state) { [types.REQUEST_CUSTOM_STAGE_FORM_DATA](state) {
state.isAddingCustomStage = true; state.isAddingCustomStage = true;
state.isEmptyStage = false; state.isEmptyStage = false;
state.isLoadingStage = false; state.isLoadingStageForm = true;
}, },
[types.HIDE_CUSTOM_STAGE_FORM](state) { [types.HIDE_CUSTOM_STAGE_FORM](state) {
state.isAddingCustomStage = false; state.isAddingCustomStage = false;
state.isEmptyStage = false; },
state.isLoadingStage = false; [types.RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS](state, data = []) {
state.labels = data.map(convertObjectPropsToCamelCase);
state.isLoadingStageForm = false;
},
[types.RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR](state) {
state.isLoadingStageForm = false;
state.labels = [];
}, },
}; };
...@@ -10,6 +10,7 @@ export default () => ({ ...@@ -10,6 +10,7 @@ export default () => ({
isLoading: false, isLoading: false,
isLoadingStage: false, isLoadingStage: false,
isLoadingStageForm: false,
isEmptyStage: false, isEmptyStage: false,
errorCode: null, errorCode: null,
...@@ -20,7 +21,11 @@ export default () => ({ ...@@ -20,7 +21,11 @@ export default () => ({
selectedProjectIds: [], selectedProjectIds: [],
selectedStageName: null, selectedStageName: null,
events: [], currentStageEvents: [],
stages: [], stages: [],
summary: [], summary: [],
labels: [],
customStageFormEvents: [],
}); });
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import Vue from 'vue';
import store from 'ee/analytics/cycle_analytics/store'; import store from 'ee/analytics/cycle_analytics/store';
import Component from 'ee/analytics/cycle_analytics/components/base.vue'; import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
...@@ -21,22 +22,49 @@ const emptyStateSvgPath = 'path/to/empty/state'; ...@@ -21,22 +22,49 @@ const emptyStateSvgPath = 'path/to/empty/state';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
function createComponent({ opts = {}, shallow = true, withStageSelected = false } = {}) {
const func = shallow ? shallowMount : mount;
const comp = func(Component, {
localVue,
store,
sync: false,
propsData: {
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
},
...opts,
});
if (withStageSelected) {
comp.vm.$store.dispatch('setSelectedGroup', {
...mockData.group,
});
comp.vm.$store.dispatch('receiveCycleAnalyticsDataSuccess', {
...mockData.cycleAnalyticsData,
});
comp.vm.$store.dispatch('receiveStageDataSuccess', {
events: mockData.issueEvents,
});
}
return comp;
}
describe('Cycle Analytics component', () => { describe('Cycle Analytics component', () => {
let wrapper; let wrapper;
let mock; let mock;
const selectStageNavItem = index =>
wrapper
.find(StageTable)
.findAll('.stage-nav-item')
.at(index);
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = shallowMount(localVue.extend(Component), { wrapper = createComponent();
localVue,
store,
sync: false,
propsData: {
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -66,17 +94,7 @@ describe('Cycle Analytics component', () => { ...@@ -66,17 +94,7 @@ describe('Cycle Analytics component', () => {
describe('after a filter has been selected', () => { describe('after a filter has been selected', () => {
describe('the user has access to the group', () => { describe('the user has access to the group', () => {
beforeEach(() => { beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', { wrapper = createComponent({ withStageSelected: true });
...mockData.group,
});
wrapper.vm.$store.dispatch('receiveCycleAnalyticsDataSuccess', {
...mockData.cycleAnalyticsData,
});
wrapper.vm.$store.dispatch('receiveStageDataSuccess', {
events: mockData.issueEvents,
});
}); });
it('hides the empty state', () => { it('hides the empty state', () => {
...@@ -92,8 +110,56 @@ describe('Cycle Analytics component', () => { ...@@ -92,8 +110,56 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(SummaryTable).exists()).toBe(true); expect(wrapper.find(SummaryTable).exists()).toBe(true);
}); });
it('displays the stage table', () => { it('does not display the add stage button', () => {
expect(wrapper.find(StageTable).exists()).toBe(true); expect(wrapper.find('.js-add-stage-button').exists()).toBe(false);
});
describe('StageTable', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: {
'stage-event-list': true,
'summary-table': true,
'add-stage-button': true,
'stage-table-header': true,
},
},
shallow: false,
withStageSelected: true,
});
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('displays the stage table', () => {
expect(wrapper.find(StageTable).exists()).toBe(true);
});
it('has the first stage selected by default', () => {
const first = selectStageNavItem(0);
const second = selectStageNavItem(1);
expect(first.classes('active')).toBe(true);
expect(second.classes('active')).toBe(false);
});
it('can navigate to different stages', done => {
selectStageNavItem(2).trigger('click');
Vue.nextTick(() => {
const first = selectStageNavItem(0);
const third = selectStageNavItem(2);
expect(third.classes('active')).toBe(true);
expect(first.classes('active')).toBe(false);
done();
});
});
}); });
}); });
...@@ -112,6 +178,49 @@ describe('Cycle Analytics component', () => { ...@@ -112,6 +178,49 @@ describe('Cycle Analytics component', () => {
expect(emptyState.exists()).toBe(true); expect(emptyState.exists()).toBe(true);
expect(emptyState.props('svgPath')).toBe(noAccessSvgPath); expect(emptyState.props('svgPath')).toBe(noAccessSvgPath);
}); });
it('will not render the summary table', () => {
expect(wrapper.find('.js-summary-table').exists()).toBe(false);
});
it('will not render the stage table', () => {
expect(wrapper.find('.js-stage-table').exists()).toBe(false);
});
it('does not display the add stage button', () => {
expect(wrapper.find('.js-add-stage-button').exists()).toBe(false);
});
});
describe('with customizableCycleAnalytics=true', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: {
'summary-table': true,
'stage-event-list': true,
'stage-nav-item': true,
},
provide: {
glFeatures: {
customizableCycleAnalytics: true,
},
},
},
shallow: false,
withStageSelected: true,
});
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
it('will display the add stage button', () => {
expect(wrapper.find('.js-add-stage-button').exists()).toBe(true);
});
}); });
}); });
}); });
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue'; import LabelsSelector from 'ee/analytics/cycle_analytics/components/labels_selector.vue';
import { mockLabels } from '../../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data'; import { groupLabels } from '../mock_data';
const labels = mockLabels.map(({ title, ...rest }) => ({ ...rest, name: title })); const selectedLabel = groupLabels[groupLabels.length - 1];
const selectedLabel = labels[labels.length - 1];
describe('Cycle Analytics LabelsSelector', () => { describe('Cycle Analytics LabelsSelector', () => {
function createComponent({ props = {}, shallow = true } = {}) { function createComponent({ props = {}, shallow = true } = {}) {
const func = shallow ? shallowMount : mount; const func = shallow ? shallowMount : mount;
return func(LabelsSelector, { return func(LabelsSelector, {
propsData: { propsData: {
labels, labels: groupLabels,
selectedLabelId: props.selectedLabelId || null, selectedLabelId: props.selectedLabelId || null,
}, },
sync: false, sync: false,
...@@ -18,6 +17,7 @@ describe('Cycle Analytics LabelsSelector', () => { ...@@ -18,6 +17,7 @@ describe('Cycle Analytics LabelsSelector', () => {
} }
let wrapper = null; let wrapper = null;
const labelNames = groupLabels.map(({ name }) => name);
describe('with no item selected', () => { describe('with no item selected', () => {
beforeEach(() => { beforeEach(() => {
...@@ -27,13 +27,9 @@ describe('Cycle Analytics LabelsSelector', () => { ...@@ -27,13 +27,9 @@ describe('Cycle Analytics LabelsSelector', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('will generate the list of labels', () => {
// includes the blank option 'Select a label'
expect(wrapper.findAll('gldropdownitem-stub').length).toEqual(labels.length + 1);
labels.forEach(({ name }) => { it.each(labelNames)('generate a label item for the label %s', name => {
expect(wrapper.text()).toContain(name); expect(wrapper.text()).toContain(name);
});
}); });
it('will render with the default option selected', () => { it('will render with the default option selected', () => {
...@@ -55,7 +51,7 @@ describe('Cycle Analytics LabelsSelector', () => { ...@@ -55,7 +51,7 @@ describe('Cycle Analytics LabelsSelector', () => {
elem.trigger('click'); elem.trigger('click');
expect(wrapper.emitted('selectLabel').length > 0).toBe(true); expect(wrapper.emitted('selectLabel').length > 0).toBe(true);
expect(wrapper.emitted('selectLabel')[0]).toContain(mockLabels[1].id); expect(wrapper.emitted('selectLabel')[0]).toContain(groupLabels[1].id);
}); });
it('will emit the "clearLabel" event if it is the default item', () => { it('will emit the "clearLabel" event if it is the default item', () => {
...@@ -77,6 +73,7 @@ describe('Cycle Analytics LabelsSelector', () => { ...@@ -77,6 +73,7 @@ describe('Cycle Analytics LabelsSelector', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('will set the active class', () => { it('will set the active class', () => {
const activeItem = wrapper.find('[active="true"]'); const activeItem = wrapper.find('[active="true"]');
......
...@@ -23,6 +23,8 @@ const generateEvents = n => ...@@ -23,6 +23,8 @@ const generateEvents = n =>
.fill(issueEvents[0]) .fill(issueEvents[0])
.map((ev, k) => ({ ...ev, title: `event-${k}`, id: k })); .map((ev, k) => ({ ...ev, title: `event-${k}`, id: k }));
const bulkEvents = generateEvents(50);
const mockStubs = { const mockStubs = {
'stage-event-item': true, 'stage-event-item': true,
'stage-build-item': true, 'stage-build-item': true,
...@@ -54,8 +56,9 @@ describe('Stage', () => { ...@@ -54,8 +56,9 @@ describe('Stage', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
props: { props: {
events: generateEvents(50), events: bulkEvents,
}, },
stubs: mockStubs,
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue'; import StageTable from 'ee/analytics/cycle_analytics/components/stage_table.vue';
import { issueEvents, issueStage, allowedStages } from '../mock_data'; import { issueEvents, issueStage, allowedStages, groupLabels } from '../mock_data';
let wrapper = null; let wrapper = null;
const $sel = { const $sel = {
...@@ -25,14 +25,16 @@ function createComponent(props = {}, shallow = false) { ...@@ -25,14 +25,16 @@ function createComponent(props = {}, shallow = false) {
propsData: { propsData: {
stages: allowedStages, stages: allowedStages,
currentStage: issueStage, currentStage: issueStage,
events: issueEvents, currentStageEvents: issueEvents,
isLoadingStage: false, labels: groupLabels,
isLoading: false,
isEmptyStage: false, isEmptyStage: false,
isUserAllowed: true, isUserAllowed: true,
isAddingCustomStage: false, isAddingCustomStage: false,
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
canEditStages: false, canEditStages: false,
customStageFormEvents: [],
...props, ...props,
}, },
stubs: { stubs: {
...@@ -118,12 +120,10 @@ describe('StageTable', () => { ...@@ -118,12 +120,10 @@ describe('StageTable', () => {
selectStage(1); selectStage(1);
Vue.nextTick() Vue.nextTick(() => {
.then(() => { expect(wrapper.emitted().selectStage.length).toEqual(1);
expect(wrapper.emitted().selectStage.length).toEqual(1); done();
}) });
.then(done)
.catch(done.fail);
}); });
it('will emit `selectStage` with the new stage title', done => { it('will emit `selectStage` with the new stage title', done => {
...@@ -131,19 +131,17 @@ describe('StageTable', () => { ...@@ -131,19 +131,17 @@ describe('StageTable', () => {
selectStage(1); selectStage(1);
Vue.nextTick() Vue.nextTick(() => {
.then(() => { const [params] = wrapper.emitted('selectStage')[0];
const [params] = wrapper.emitted('selectStage')[0]; expect(params).toMatchObject({ title: secondStage.title });
expect(params).toMatchObject({ title: secondStage.title }); done();
}) });
.then(done)
.catch(done.fail);
}); });
}); });
}); });
it('isLoadingStage = true', () => { it('isLoading = true', () => {
wrapper = createComponent({ isLoadingStage: true }, true); wrapper = createComponent({ isLoading: true }, true);
expect(wrapper.find('gl-loading-icon-stub').exists()).toEqual(true); expect(wrapper.find('gl-loading-icon-stub').exists()).toEqual(true);
}); });
......
...@@ -3,6 +3,9 @@ import { getJSONFixture } from 'helpers/fixtures'; ...@@ -3,6 +3,9 @@ import { getJSONFixture } from 'helpers/fixtures';
import mutations from 'ee/analytics/cycle_analytics/store/mutations'; import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mockLabels } from '../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data';
export const groupLabels = mockLabels.map(({ title, ...rest }) => ({ ...rest, name: title }));
export const group = { export const group = {
id: 1, id: 1,
......
...@@ -4,10 +4,12 @@ import testAction from 'helpers/vuex_action_helper'; ...@@ -4,10 +4,12 @@ import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/analytics/cycle_analytics/store/actions'; import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { group, cycleAnalyticsData, allowedStages as stages } from '../mock_data'; import { group, cycleAnalyticsData, allowedStages as stages, groupLabels } from '../mock_data';
const stageData = { events: [] }; const stageData = { events: [] };
const error = new Error('Request failed with status code 404'); const error = new Error('Request failed with status code 404');
const groupPath = 'cool-group';
const groupLabelsEndpoint = `/groups/${groupPath}/-/labels`;
describe('Cycle analytics actions', () => { describe('Cycle analytics actions', () => {
let state; let state;
...@@ -143,6 +145,59 @@ describe('Cycle analytics actions', () => { ...@@ -143,6 +145,59 @@ describe('Cycle analytics actions', () => {
}); });
}); });
describe('fetchCustomStageFormData', () => {
beforeEach(() => {
mock.onGet(groupLabelsEndpoint).replyOnce(200, groupLabels);
});
it('dispatches receiveCustomStageFormData if the request succeeds', done => {
testAction(
actions.fetchCustomStageFormData,
groupPath,
state,
[],
[
{ type: 'requestCustomStageFormData' },
{
type: 'receiveCustomStageFormDataSuccess',
payload: groupLabels,
},
],
done,
);
});
it('dispatches receiveCustomStageFormDataError if the request fails', done => {
testAction(
actions.fetchCustomStageFormData,
'this-path-does-not-exist',
state,
[],
[
{ type: 'requestCustomStageFormData' },
{
type: 'receiveCustomStageFormDataError',
payload: error,
},
],
done,
);
});
describe('receiveCustomStageFormDataError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('flashes an error message if the request fails', () => {
actions.receiveCustomStageFormDataError({
commit: () => {},
});
shouldFlashAnError('There was an error fetching data for the form');
});
});
});
describe('fetchCycleAnalyticsData', () => { describe('fetchCycleAnalyticsData', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(state.endpoints.cycleAnalyticsData).replyOnce(200, cycleAnalyticsData); mock.onGet(state.endpoints.cycleAnalyticsData).replyOnce(200, cycleAnalyticsData);
......
...@@ -89,4 +89,26 @@ describe('Cycle analytics getters', () => { ...@@ -89,4 +89,26 @@ describe('Cycle analytics getters', () => {
expect(getters.hasNoAccessError(state)).toEqual(false); expect(getters.hasNoAccessError(state)).toEqual(false);
}); });
}); });
describe('currentGroupPath', () => {
describe('with selectedGroup set', () => {
it('returns the `full_path` value of the group', () => {
const fullPath = 'cool-beans';
state = {
selectedGroup: {
full_path: fullPath,
},
};
expect(getters.currentGroupPath(state)).toEqual(fullPath);
});
});
describe('without a selectedGroup set', () => {
it.each([[''], [{}], [null]])('given %s will return null', value => {
state = { selectedGroup: value };
expect(getters.currentGroupPath(state)).toEqual(null);
});
});
});
}); });
import mutations from 'ee/analytics/cycle_analytics/store/mutations'; import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { import {
cycleAnalyticsData, cycleAnalyticsData,
rawEvents as events, rawEvents,
issueEvents as transformedEvents, issueEvents as transformedEvents,
issueStage, issueStage,
planStage, planStage,
...@@ -10,22 +12,36 @@ import { ...@@ -10,22 +12,36 @@ import {
stagingStage, stagingStage,
reviewStage, reviewStage,
productionStage, productionStage,
groupLabels,
} from '../mock_data'; } from '../mock_data';
let state = null;
describe('Cycle analytics mutations', () => { describe('Cycle analytics mutations', () => {
beforeEach(() => {
state = {};
});
afterEach(() => {
state = null;
});
it.each` it.each`
mutation | stateKey | value mutation | stateKey | value
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true} ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${true} ${types.REQUEST_CUSTOM_STAGE_FORM_DATA} | ${'isAddingCustomStage'} | ${true}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${false} ${types.HIDE_CUSTOM_STAGE_FORM} | ${'isAddingCustomStage'} | ${false}
${types.REQUEST_CUSTOM_STAGE_FORM_DATA} | ${'isLoadingStageForm'} | ${true}
${types.RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR} | ${'isLoadingStageForm'} | ${false}
${types.RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS} | ${'isLoadingStageForm'} | ${false}
${types.RECEIVE_CUSTOM_STAGE_FORM_DATA_ERROR} | ${'labels'} | ${[]}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => { `('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
const state = {};
mutations[mutation](state); mutations[mutation](state);
expect(state[stateKey]).toBe(value); expect(state[stateKey]).toEqual(value);
}); });
it.each` it.each`
...@@ -39,7 +55,7 @@ describe('Cycle analytics mutations', () => { ...@@ -39,7 +55,7 @@ describe('Cycle analytics mutations', () => {
`( `(
'$mutation with payload $payload will update state with $expectedState', '$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => { ({ mutation, payload, expectedState }) => {
const state = { endpoints: { cycleAnalyticsData: '/fake/api' } }; state = { endpoints: { cycleAnalyticsData: '/fake/api' } };
mutations[mutation](state, payload); mutations[mutation](state, payload);
expect(state).toMatchObject(expectedState); expect(state).toMatchObject(expectedState);
...@@ -47,21 +63,41 @@ describe('Cycle analytics mutations', () => { ...@@ -47,21 +63,41 @@ describe('Cycle analytics mutations', () => {
); );
describe(`${types.RECEIVE_STAGE_DATA_SUCCESS}`, () => { describe(`${types.RECEIVE_STAGE_DATA_SUCCESS}`, () => {
it('will set the events state item with the camelCased events', () => { it('will set the currentStageEvents state item with the camelCased events', () => {
const state = {}; mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state, { events: rawEvents });
expect(state.currentStageEvents).toEqual(transformedEvents);
});
it('will set isLoadingStage=false', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state);
expect(state.isLoadingStage).toEqual(false);
});
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state, { events }); it('will set isEmptyStage=false if currentStageEvents.length > 0', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state, { events: rawEvents });
expect(state.events).toEqual(transformedEvents); expect(state.isEmptyStage).toEqual(false);
expect(state.isLoadingStage).toBe(false); });
expect(state.isEmptyStage).toBe(false);
it('will set isEmptyStage=true if currentStageEvents.length <= 0', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state);
expect(state.isEmptyStage).toEqual(true);
});
});
describe(`${types.RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS}`, () => {
it('will set the labels state item with the camelCased custom stage events', () => {
mutations[types.RECEIVE_CUSTOM_STAGE_FORM_DATA_SUCCESS](state, groupLabels);
expect(state.labels).toEqual(groupLabels.map(convertObjectPropsToCamelCase));
}); });
}); });
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => { describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS}`, () => {
it('will set isLoading=false and errorCode=null', () => { it('will set isLoading=false and errorCode=null', () => {
const state = {};
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, { mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
stats: [], stats: [],
summary: [], summary: [],
...@@ -74,8 +110,6 @@ describe('Cycle analytics mutations', () => { ...@@ -74,8 +110,6 @@ describe('Cycle analytics mutations', () => {
describe('with data', () => { describe('with data', () => {
it('will convert the stats object to stages', () => { it('will convert the stats object to stages', () => {
const state = {};
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, cycleAnalyticsData); mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, cycleAnalyticsData);
[issueStage, planStage, codeStage, stagingStage, reviewStage, productionStage].forEach( [issueStage, planStage, codeStage, stagingStage, reviewStage, productionStage].forEach(
...@@ -86,8 +120,6 @@ describe('Cycle analytics mutations', () => { ...@@ -86,8 +120,6 @@ describe('Cycle analytics mutations', () => {
}); });
it('will set the selectedStageName to the name of the first stage', () => { it('will set the selectedStageName to the name of the first stage', () => {
const state = {};
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, cycleAnalyticsData); mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, cycleAnalyticsData);
expect(state.selectedStageName).toEqual('issue'); expect(state.selectedStageName).toEqual('issue');
...@@ -96,8 +128,6 @@ describe('Cycle analytics mutations', () => { ...@@ -96,8 +128,6 @@ describe('Cycle analytics mutations', () => {
it('will set each summary item with a value of 0 to "-"', () => { it('will set each summary item with a value of 0 to "-"', () => {
// { value: '-', title: 'New Issues' }, { value: '-', title: 'Deploys' } // { value: '-', title: 'New Issues' }, { value: '-', title: 'Deploys' }
const state = {};
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, { mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, {
...cycleAnalyticsData, ...cycleAnalyticsData,
summary: [{ value: 0, title: 'New Issues' }, { value: 0, title: 'Deploys' }], summary: [{ value: 0, title: 'New Issues' }, { value: 0, title: 'Deploys' }],
...@@ -113,7 +143,6 @@ describe('Cycle analytics mutations', () => { ...@@ -113,7 +143,6 @@ describe('Cycle analytics mutations', () => {
describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR}`, () => { describe(`${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR}`, () => {
it('sets errorCode correctly', () => { it('sets errorCode correctly', () => {
const state = {};
const errorCode = 403; const errorCode = 403;
mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state, errorCode); mutations[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state, errorCode);
......
...@@ -15839,7 +15839,7 @@ msgstr "" ...@@ -15839,7 +15839,7 @@ msgstr ""
msgid "There was an error fetching configuration for charts" msgid "There was an error fetching configuration for charts"
msgstr "" msgstr ""
msgid "There was an error fetching the form data" msgid "There was an error fetching data for the form"
msgstr "" msgstr ""
msgid "There was an error gathering the chart data" msgid "There was an error gathering the chart data"
......
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