Commit 419bf867 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Fix instance VSA specs

Checks that we have a group before
rendering the filter bar

Move feature flag setting to initialize action

Makes sure we set the feature flag state
before we mount the component
parent adcaabed
......@@ -2,7 +2,6 @@
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PROJECTS_PER_PAGE, STAGE_ACTIONS } from '../constants';
import GroupsDropdownFilter from '../../shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from '../../shared/components/projects_dropdown_filter.vue';
......@@ -40,7 +39,7 @@ export default {
MetricCard,
FilterBar,
},
mixins: [glFeatureFlagsMixin(), UrlSyncMixin],
mixins: [UrlSyncMixin],
props: {
emptyStateSvgPath: {
type: String,
......@@ -106,7 +105,10 @@ export default {
return !this.hasNoAccessError && !this.isLoading;
},
shouldDsiplayPathNavigation() {
return this.featureFlags.hasPathNavigation && !this.hasNoAccessError;
return this.featureFlags.hasPathNavigation && !this.hasNoAccessError && this.selectedStage;
},
shouldDisplayFilterBar() {
return this.featureFlags.hasFilterBar && this.currentGroupPath;
},
isLoadingTypeOfWork() {
return this.isLoadingTasksByTypeChartTopLabels || this.isLoadingTasksByTypeChart;
......@@ -133,23 +135,7 @@ export default {
return this.selectedProjectIds.length > 0;
},
},
mounted() {
const {
glFeatures: {
cycleAnalyticsScatterplotEnabled: hasDurationChart,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian,
valueStreamAnalyticsPathNavigation: hasPathNavigation,
valueStreamAnalyticsFilterBar: hasFilterBar,
},
} = this;
this.setFeatureFlags({
hasDurationChart,
hasDurationChartMedian,
hasPathNavigation,
hasFilterBar,
});
},
methods: {
...mapActions([
'fetchCycleAnalyticsData',
......@@ -160,9 +146,9 @@ export default {
'setDateRange',
'updateStage',
'removeStage',
'setFeatureFlags',
'updateStage',
'reorderStage',
'setSelectedFilters',
]),
...mapActions('customStages', [
'hideForm',
......@@ -259,7 +245,7 @@ export default {
/>
</div>
<filter-bar
v-if="featureFlags.hasFilterBar"
v-if="shouldDisplayFilterBar"
class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 mt-md-0 gl-mr-3"
:disabled="!hasProject"
/>
......
......@@ -100,9 +100,6 @@ export default {
];
},
},
created() {
this.fetchTokenData();
},
mounted() {
this.initializeTokens();
},
......
......@@ -9,7 +9,17 @@ export default () => {
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath, hideGroupDropDown } = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore();
store.dispatch('initializeCycleAnalytics', initialData);
const {
cycleAnalyticsScatterplotEnabled: hasDurationChart = false,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian = false,
valueStreamAnalyticsPathNavigation: hasPathNavigation = false,
valueStreamAnalyticsFilterBar: hasFilterBar = false,
} = gon?.features;
store.dispatch('initializeCycleAnalytics', {
...initialData,
featureFlags: { hasDurationChart, hasDurationChartMedian, hasPathNavigation, hasFilterBar },
});
return new Vue({
el,
......
......@@ -8,11 +8,19 @@ import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../ut
export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
export const setSelectedGroup = ({ commit, dispatch, state }, group) => {
commit(types.SET_SELECTED_GROUP, group);
const { featureFlags } = state;
if (featureFlags?.hasFilterBar) {
return dispatch('filters/initialize', {
groupPath: group.full_path,
});
}
return Promise.resolve();
};
export const setSelectedProjects = ({ commit }, projects) => {
export const setSelectedProjects = ({ commit }, projects) =>
commit(types.SET_SELECTED_PROJECTS, projects);
};
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
......@@ -234,9 +242,10 @@ export const removeStage = ({ dispatch, state }, stageId) => {
.catch(error => dispatch('receiveRemoveStageError', error));
};
export const setSelectedFilters = ({ commit, dispatch }, filters = {}) => {
export const setSelectedFilters = ({ commit, dispatch, getters }, filters = {}) => {
commit(types.SET_SELECTED_FILTERS, filters);
if (filters?.group?.fullPath) {
const { currentGroupPath } = getters;
if (currentGroupPath) {
return dispatch('fetchCycleAnalyticsData');
}
return Promise.resolve();
......@@ -248,9 +257,17 @@ export const initializeCycleAnalyticsSuccess = ({ commit }) =>
export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) => {
commit(types.INITIALIZE_CYCLE_ANALYTICS, initialData);
dispatch('filters/initialize', initialData);
const { featureFlags = {} } = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags);
if (initialData.group?.fullPath) {
if (featureFlags?.hasFilterBar) {
dispatch('filters/initialize', {
groupPath: initialData.group.fullPath,
...initialData,
});
}
if (initialData?.group?.fullPath) {
return Promise.resolve()
.then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => dispatch('initializeCycleAnalyticsSuccess'));
......
......@@ -6,9 +6,11 @@ import * as types from './mutation_types';
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ commit }, { milestonesPath = '', labelsPath = '' }) => {
commit(types.SET_MILESTONES_PATH, appendExtension(milestonesPath));
commit(types.SET_LABELS_PATH, appendExtension(labelsPath));
export const setPaths = ({ commit }, { groupPath = '', milestonesPath = '', labelsPath = '' }) => {
const ms = milestonesPath || `/groups/${groupPath}/-/milestones`;
const ls = labelsPath || `/groups/${groupPath}/-/labels`;
commit(types.SET_MILESTONES_PATH, appendExtension(ms));
commit(types.SET_LABELS_PATH, appendExtension(ls));
};
export const fetchTokenData = ({ dispatch }) => {
......@@ -88,5 +90,6 @@ export const initialize = ({ dispatch, commit }, initialFilters) => {
commit(types.INITIALIZE, initialFilters);
return Promise.resolve()
.then(() => dispatch('setPaths', initialFilters))
.then(() => dispatch('setFilters', initialFilters));
.then(() => dispatch('setFilters', initialFilters))
.then(() => dispatch('fetchTokenData'));
};
......@@ -30,8 +30,6 @@ import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
const emptyStateSvgPath = 'path/to/empty/state';
const milestonesPath = '/some/milestones/endpoint';
const labelsPath = '/some/labels/endpoint';
const hideGroupDropDown = false;
const selectedGroup = convertObjectPropsToCamelCase(mockData.group);
......@@ -48,6 +46,13 @@ const defaultStubs = {
GroupsDropdownFilter: true,
};
const defaultFeatureFlags = {
hasDurationChart: true,
hasDurationChartMedian: true,
hasPathNavigation: false,
hasFilterBar: false,
};
const initialCycleAnalyticsState = {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
......@@ -55,8 +60,6 @@ const initialCycleAnalyticsState = {
selectedAuthor: null,
selectedAssignees: [],
selectedLabels: [],
milestonesPath,
labelsPath,
group: selectedGroup,
};
......@@ -66,9 +69,7 @@ function createComponent({
},
shallow = true,
withStageSelected = false,
scatterplotEnabled = true,
pathNavigationEnabled = false,
filterBarEnabled = false,
featureFlags = {},
props = {},
} = {}) {
const func = shallow ? shallowMount : mount;
......@@ -84,19 +85,16 @@ function createComponent({
hideGroupDropDown,
...props,
},
provide: {
glFeatures: {
cycleAnalyticsScatterplotEnabled: scatterplotEnabled,
valueStreamAnalyticsPathNavigation: pathNavigationEnabled,
valueStreamAnalyticsFilterBar: filterBarEnabled,
},
},
...opts,
});
comp.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
featureFlags: {
...defaultFeatureFlags,
...featureFlags,
},
});
if (withStageSelected) {
......@@ -173,11 +171,11 @@ describe('Cycle Analytics component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent();
wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
wrapper = createComponent({
featureFlags: {
hasPathNavigation: true,
hasFilterBar: true,
},
});
});
......@@ -257,6 +255,10 @@ describe('Cycle Analytics component', () => {
mock = new MockAdapter(axios);
wrapper = createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: true,
hasFilterBar: true,
},
});
});
......@@ -321,6 +323,15 @@ describe('Cycle Analytics component', () => {
describe('path navigation', () => {
describe('disabled', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
featureFlags: {
hasPathNavigation: false,
},
});
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
......@@ -330,7 +341,9 @@ describe('Cycle Analytics component', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
pathNavigationEnabled: true,
featureFlags: {
hasPathNavigation: true,
},
});
});
......@@ -342,6 +355,15 @@ describe('Cycle Analytics component', () => {
describe('filter bar', () => {
describe('disabled', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
featureFlags: {
hasFilterBar: false,
},
});
});
it('does not display the filter bar', () => {
displaysFilterBar(false);
});
......@@ -351,7 +373,9 @@ describe('Cycle Analytics component', () => {
beforeEach(() => {
wrapper = createComponent({
withStageSelected: true,
filterBarEnabled: true,
featureFlags: {
hasFilterBar: true,
},
});
});
......@@ -364,6 +388,7 @@ describe('Cycle Analytics component', () => {
describe('StageTable', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent({
opts: {
stubs: {
......
......@@ -18,11 +18,9 @@ describe('Filter bar', () => {
let store;
let setFiltersMock;
let fetchTokenDataMock;
const createStore = (initialState = {}) => {
setFiltersMock = jest.fn();
fetchTokenDataMock = jest.fn();
return new Vuex.Store({
modules: {
......@@ -34,7 +32,6 @@ describe('Filter bar', () => {
},
actions: {
setFilters: setFiltersMock,
fetchTokenData: fetchTokenDataMock,
},
},
},
......@@ -69,10 +66,6 @@ describe('Filter bar', () => {
it('renders GlFilteredSearch component', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
it('fetches the token data', () => {
expect(fetchTokenDataMock).toHaveBeenCalled();
});
});
describe('when the state has data', () => {
......
......@@ -56,7 +56,6 @@ describe('Cycle analytics actions', () => {
it.each`
action | type | stateKey | payload
${'setFeatureFlags'} | ${'SET_FEATURE_FLAGS'} | ${'featureFlags'} | ${{ hasDurationChart: true }}
${'setSelectedGroup'} | ${'SET_SELECTED_GROUP'} | ${'selectedGroup'} | ${'someNewGroup'}
${'setSelectedProjects'} | ${'SET_SELECTED_PROJECTS'} | ${'selectedProjectIds'} | ${[10, 20, 30, 40]}
${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${'selectedStage'} | ${{ id: 'someStageId' }}
`('$action should set $stateKey with $payload and type $type', ({ action, type, payload }) => {
......@@ -88,6 +87,48 @@ describe('Cycle analytics actions', () => {
});
});
describe('setSelectedGroup', () => {
it('commits the setSelectedGroup mutation', () => {
return testAction(
actions.setSelectedGroup,
{ ...selectedGroup },
state,
[{ type: types.SET_SELECTED_GROUP, payload: selectedGroup }],
[],
);
});
describe('with hasFilterBar=true', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
...state.featureFlags,
hasFilterBar: true,
},
};
mock = new MockAdapter(axios);
});
it('commits the setSelectedGroup mutation', () => {
return testAction(
actions.setSelectedGroup,
{ full_path: selectedGroup.fullPath },
state,
[{ type: types.SET_SELECTED_GROUP, payload: { full_path: selectedGroup.fullPath } }],
[
{
type: 'filters/initialize',
payload: {
groupPath: selectedGroup.fullPath,
},
},
],
);
});
});
});
describe('fetchStageData', () => {
beforeEach(() => {
state = { ...state, selectedGroup };
......
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from 'ee/analytics/cycle_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/cycle_analytics/store/modules/filters/mutation_types';
import initialState from 'ee/analytics/cycle_analytics/store/modules/filters/state';
......@@ -33,13 +34,17 @@ describe('Filters actions', () => {
selectedMilestone: 'NEXT',
};
it('initializes the state and dispatches setPaths and setFilters', () => {
it('initializes the state and dispatches setPaths, setFilters and fetchTokenData', () => {
return testAction(
actions.initialize,
initialData,
state,
[{ type: types.INITIALIZE, payload: initialData }],
[{ type: 'setPaths', payload: initialData }, { type: 'setFilters', payload: initialData }],
[
{ type: 'setPaths', payload: initialData },
{ type: 'setFilters', payload: initialData },
{ type: 'fetchTokenData' },
],
);
});
});
......@@ -119,7 +124,7 @@ describe('Filters actions', () => {
describe('fetchAuthors', () => {
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(200, filterUsers);
mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
});
it('dispatches RECEIVE_AUTHORS_SUCCESS with received data', () => {
......@@ -138,7 +143,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(500);
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_AUTHORS_ERROR', () => {
......@@ -150,7 +155,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
payload: 500,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
......@@ -162,7 +167,7 @@ describe('Filters actions', () => {
describe('fetchMilestones', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(milestonesPath).replyOnce(200, filterMilestones);
mock.onGet(milestonesPath).replyOnce(httpStatusCodes.OK, filterMilestones);
});
it('dispatches RECEIVE_MILESTONES_SUCCESS with received data', () => {
......@@ -181,7 +186,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(500);
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_MILESTONES_ERROR', () => {
......@@ -193,7 +198,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_MILESTONES },
{
type: types.RECEIVE_MILESTONES_ERROR,
payload: 500,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
......@@ -205,7 +210,7 @@ describe('Filters actions', () => {
describe('fetchAssignees', () => {
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(200, filterUsers);
mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
});
it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data', () => {
......@@ -224,7 +229,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(500);
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_ASSIGNEES_ERROR', () => {
......@@ -236,7 +241,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
payload: 500,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
......@@ -248,7 +253,7 @@ describe('Filters actions', () => {
describe('fetchLabels', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(labelsPath).replyOnce(200, filterLabels);
mock.onGet(labelsPath).replyOnce(httpStatusCodes.OK, filterLabels);
});
it('dispatches RECEIVE_LABELS_SUCCESS with received data', () => {
......@@ -267,7 +272,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(500);
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_LABELS_ERROR', () => {
......@@ -279,7 +284,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_LABELS },
{
type: types.RECEIVE_LABELS_ERROR,
payload: 500,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
},
],
[],
......
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