Commit 8483dda7 authored by Simon Knox's avatar Simon Knox

Merge branch '230631-223735-mlunoe-clean-up-instance-level-value-stream-analytics' into 'master'

Remove (dead) instance level Value Stream Analytics page code

Closes #223735 and #230631

See merge request gitlab-org/gitlab!42232
parents 761f71de 4dd08265
<script> <script>
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 { PROJECTS_PER_PAGE } from '../constants'; import { PROJECTS_PER_PAGE } from '../constants';
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 { SIMILARITY_ORDER, LAST_ACTIVITY_AT, DATE_RANGE_LIMIT } from '../../shared/constants'; import { SIMILARITY_ORDER, LAST_ACTIVITY_AT, DATE_RANGE_LIMIT } from '../../shared/constants';
import DateRange from '../../shared/components/daterange.vue'; import DateRange from '../../shared/components/daterange.vue';
...@@ -25,7 +23,6 @@ export default { ...@@ -25,7 +23,6 @@ export default {
DateRange, DateRange,
DurationChart, DurationChart,
GlEmptyState, GlEmptyState,
GroupsDropdownFilter,
ProjectsDropdownFilter, ProjectsDropdownFilter,
StageTable, StageTable,
TypeOfWorkCharts, TypeOfWorkCharts,
...@@ -50,10 +47,6 @@ export default { ...@@ -50,10 +47,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
hideGroupDropDown: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -61,7 +54,7 @@ export default { ...@@ -61,7 +54,7 @@ export default {
'isLoading', 'isLoading',
'isLoadingStage', 'isLoadingStage',
'isEmptyStage', 'isEmptyStage',
'selectedGroup', 'currentGroup',
'selectedProjects', 'selectedProjects',
'selectedStage', 'selectedStage',
'stages', 'stages',
...@@ -87,10 +80,10 @@ export default { ...@@ -87,10 +80,10 @@ export default {
]), ]),
...mapGetters('customStages', ['customStageFormActive']), ...mapGetters('customStages', ['customStageFormActive']),
shouldRenderEmptyState() { shouldRenderEmptyState() {
return !this.selectedGroup && !this.isLoading; return !this.currentGroup && !this.isLoading;
}, },
shouldDisplayFilters() { shouldDisplayFilters() {
return this.selectedGroup && !this.errorCode; return !this.errorCode;
}, },
shouldDisplayDurationChart() { shouldDisplayDurationChart() {
return this.featureFlags.hasDurationChart && !this.hasNoAccessError; return this.featureFlags.hasDurationChart && !this.hasNoAccessError;
...@@ -101,11 +94,6 @@ export default { ...@@ -101,11 +94,6 @@ export default {
shouldDisplayPathNavigation() { shouldDisplayPathNavigation() {
return this.featureFlags.hasPathNavigation && !this.hasNoAccessError && this.selectedStage; return this.featureFlags.hasPathNavigation && !this.hasNoAccessError && this.selectedStage;
}, },
shouldDisplayFilterBar() {
// TODO: After we remove instance VSA currentGroupPath will be always set
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
return this.currentGroupPath;
},
shouldDisplayCreateMultipleValueStreams() { shouldDisplayCreateMultipleValueStreams() {
return Boolean( return Boolean(
this.featureFlags.hasCreateMultipleValueStreams && !this.isLoadingValueStreams, this.featureFlags.hasCreateMultipleValueStreams && !this.isLoadingValueStreams,
...@@ -118,7 +106,6 @@ export default { ...@@ -118,7 +106,6 @@ export default {
const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null; const selectedProjectIds = this.selectedProjectIds?.length ? this.selectedProjectIds : null;
return { return {
group_id: !this.hideGroupDropDown ? this.currentGroupPath : null,
project_ids: selectedProjectIds, project_ids: selectedProjectIds,
created_after: toYmd(this.startDate), created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate), created_before: toYmd(this.endDate),
...@@ -143,26 +130,14 @@ export default { ...@@ -143,26 +130,14 @@ export default {
...mapActions([ ...mapActions([
'fetchCycleAnalyticsData', 'fetchCycleAnalyticsData',
'fetchStageData', 'fetchStageData',
'setSelectedGroup',
'setSelectedProjects', 'setSelectedProjects',
'setSelectedStage', 'setSelectedStage',
'setDateRange', 'setDateRange',
'updateStage',
'removeStage', 'removeStage',
'updateStage', 'updateStage',
'reorderStage', 'reorderStage',
]), ]),
...mapActions('customStages', [ ...mapActions('customStages', ['hideForm', 'showCreateForm', 'showEditForm', 'createStage']),
'hideForm',
'showCreateForm',
'showEditForm',
'createStage',
'clearFormErrors',
]),
onGroupSelect(group) {
this.setSelectedGroup(group);
this.fetchCycleAnalyticsData();
},
onProjectsSelect(projects) { onProjectsSelect(projects) {
this.setSelectedProjects(projects); this.setSelectedProjects(projects);
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
...@@ -194,9 +169,6 @@ export default { ...@@ -194,9 +169,6 @@ export default {
}, },
multiProjectSelect: true, multiProjectSelect: true,
dateOptions: [7, 30, 90], dateOptions: [7, 30, 90],
groupsQueryParams: {
min_access_level: featureAccessLevel.EVERYONE,
},
maxDateRange: DATE_RANGE_LIMIT, maxDateRange: DATE_RANGE_LIMIT,
}; };
</script> </script>
...@@ -211,7 +183,15 @@ export default { ...@@ -211,7 +183,15 @@ export default {
class="gl-align-self-start gl-sm-align-self-start gl-mt-0 gl-sm-mt-5" class="gl-align-self-start gl-sm-align-self-start gl-mt-0 gl-sm-mt-5"
/> />
</div> </div>
<div class="gl-max-w-full"> <gl-empty-state
v-if="shouldRenderEmptyState"
:title="__('Value Stream Analytics can help you determine your team’s velocity')"
:description="
__('Filter parameters are not valid. Make sure that the end date is after the start date.')
"
:svg-path="emptyStateSvgPath"
/>
<div v-if="!shouldRenderEmptyState" class="gl-max-w-full">
<div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom"> <div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom">
<div v-if="shouldDisplayPathNavigation" class="gl-w-full gl-pb-2"> <div v-if="shouldDisplayPathNavigation" class="gl-w-full gl-pb-2">
<path-navigation <path-navigation
...@@ -223,57 +203,35 @@ export default { ...@@ -223,57 +203,35 @@ export default {
/> />
</div> </div>
<div <div
v-if="shouldDisplayFilters"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between" class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
> >
<div class="dropdown-container d-flex flex-column flex-lg-row"> <projects-dropdown-filter
<groups-dropdown-filter :key="currentGroup.id"
v-if="!hideGroupDropDown" class="js-projects-dropdown-filter project-select"
class="js-groups-dropdown-filter" :group-id="currentGroup.id"
:class="{ 'mr-lg-3': shouldDisplayFilters }" :query-params="projectsQueryParams"
:query-params="$options.groupsQueryParams" :multi-select="$options.multiProjectSelect"
:default-group="selectedGroup" :default-projects="selectedProjects"
@selected="onGroupSelect" @selected="onProjectsSelect"
/> />
<projects-dropdown-filter <date-range
v-if="shouldDisplayFilters" :start-date="startDate"
:key="selectedGroup.id" :end-date="endDate"
class="js-projects-dropdown-filter project-select" :max-date-range="$options.maxDateRange"
:group-id="selectedGroup.id" :include-selected-date="true"
:query-params="projectsQueryParams" class="js-daterange-picker"
:multi-select="$options.multiProjectSelect" @change="setDateRange"
:default-projects="selectedProjects" />
@selected="onProjectsSelect"
/>
</div>
<div v-if="shouldDisplayFilters" class="gl-justify-content-end gl-white-space-nowrap">
<date-range
:start-date="startDate"
:end-date="endDate"
:max-date-range="$options.maxDateRange"
:include-selected-date="true"
class="js-daterange-picker"
@change="setDateRange"
/>
</div>
</div> </div>
<filter-bar <filter-bar
v-if="shouldDisplayFilterBar" v-if="shouldDisplayFilters"
class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 gl-mr-3 gl-border-none" class="js-filter-bar filtered-search-box gl-display-flex gl-mt-3 gl-mr-3 gl-border-none"
:group-path="currentGroupPath" :group-path="currentGroupPath"
/> />
</div> </div>
</div> </div>
<gl-empty-state <div v-if="!shouldRenderEmptyState" class="cycle-analytics gl-mt-0">
v-if="shouldRenderEmptyState"
:title="__('Value Stream Analytics can help you determine your team’s velocity')"
:description="
__(
'Start by choosing a group to see how your team is spending time. You can then drill down to the project level.',
)
"
:svg-path="emptyStateSvgPath"
/>
<div v-else class="cycle-analytics mt-0">
<gl-empty-state <gl-empty-state
v-if="hasNoAccessError" v-if="hasNoAccessError"
class="js-empty-state" class="js-empty-state"
......
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
startDate, startDate,
endDate, endDate,
selectedProjectIds, selectedProjectIds,
selectedGroup: { name: groupName }, currentGroup: { name: groupName },
} = this.selectedTasksByTypeFilters; } = this.selectedTasksByTypeFilters;
const selectedProjectCount = selectedProjectIds.length; const selectedProjectCount = selectedProjectIds.length;
......
...@@ -4,13 +4,12 @@ import CycleAnalytics from './components/base.vue'; ...@@ -4,13 +4,12 @@ import CycleAnalytics from './components/base.vue';
import createStore from './store'; import createStore from './store';
import { buildCycleAnalyticsInitialData } from '../shared/utils'; import { buildCycleAnalyticsInitialData } from '../shared/utils';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast); Vue.use(GlToast);
export default () => { export default () => {
const el = document.querySelector('#js-cycle-analytics-app'); const el = document.querySelector('#js-cycle-analytics-app');
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath, hideGroupDropDown } = el.dataset; const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset;
const initialData = buildCycleAnalyticsInitialData(el.dataset); const initialData = buildCycleAnalyticsInitialData(el.dataset);
const store = createStore(); const store = createStore();
const { const {
...@@ -51,7 +50,6 @@ export default () => { ...@@ -51,7 +50,6 @@ export default () => {
emptyStateSvgPath, emptyStateSvgPath,
noDataSvgPath, noDataSvgPath,
noAccessSvgPath, noAccessSvgPath,
hideGroupDropDown: parseBoolean(hideGroupDropDown),
}, },
}), }),
}); });
......
...@@ -14,16 +14,11 @@ import { ...@@ -14,16 +14,11 @@ import {
const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`); const appendExtension = path => (path.indexOf('.') > -1 ? path : `${path}.json`);
export const setPaths = ({ dispatch }, options) => { export const setPaths = ({ dispatch }, options) => {
const { group, milestonesPath = '', labelsPath = '' } = options; const { groupPath, milestonesPath = '', labelsPath = '' } = options;
// TODO: After we remove instance VSA we can rely on the paths from the BE
// https://gitlab.com/gitlab-org/gitlab/-/issues/223735
const groupPath = group?.parentId || group?.fullPath || '';
const milestonesEndpoint = milestonesPath || `/groups/${groupPath}/-/milestones`;
const labelsEndpoint = labelsPath || `/groups/${groupPath}/-/labels`;
return dispatch('filters/setEndpoints', { return dispatch('filters/setEndpoints', {
labelsEndpoint: appendExtension(labelsEndpoint), labelsEndpoint: appendExtension(labelsPath),
milestonesEndpoint: appendExtension(milestonesEndpoint), milestonesEndpoint: appendExtension(milestonesPath),
groupEndpoint: groupPath, groupEndpoint: groupPath,
}); });
}; };
...@@ -31,11 +26,6 @@ export const setPaths = ({ dispatch }, options) => { ...@@ -31,11 +26,6 @@ export const setPaths = ({ dispatch }, options) => {
export const setFeatureFlags = ({ commit }, featureFlags) => export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags); commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit, dispatch }, group) => {
commit(types.SET_SELECTED_GROUP, group);
return dispatch('filters/initialize', { groupPath: group.full_path });
};
export const setSelectedProjects = ({ commit }, projects) => export const setSelectedProjects = ({ commit }, projects) =>
commit(types.SET_SELECTED_PROJECTS, projects); commit(types.SET_SELECTED_PROJECTS, projects);
...@@ -293,12 +283,13 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) ...@@ -293,12 +283,13 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
selectedMilestone, selectedMilestone,
selectedAssigneeList, selectedAssigneeList,
selectedLabelList, selectedLabelList,
group,
} = initialData; } = initialData;
commit(types.SET_FEATURE_FLAGS, featureFlags); commit(types.SET_FEATURE_FLAGS, featureFlags);
if (initialData.group?.fullPath) { if (group?.fullPath) {
return Promise.all([ return Promise.all([
dispatch('setPaths', { group: initialData.group, milestonesPath, labelsPath }), dispatch('setPaths', { group, milestonesPath, labelsPath }),
dispatch('filters/initialize', { dispatch('filters/initialize', {
selectedAuthor, selectedAuthor,
selectedMilestone, selectedMilestone,
...@@ -311,6 +302,7 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {}) ...@@ -311,6 +302,7 @@ export const initializeCycleAnalytics = ({ dispatch, commit }, initialData = {})
.then(() => dispatch('fetchCycleAnalyticsData')) .then(() => dispatch('fetchCycleAnalyticsData'))
.then(() => dispatch('initializeCycleAnalyticsSuccess')); .then(() => dispatch('initializeCycleAnalyticsSuccess'));
} }
return dispatch('initializeCycleAnalyticsSuccess'); return dispatch('initializeCycleAnalyticsSuccess');
}; };
......
...@@ -11,7 +11,7 @@ export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDE ...@@ -11,7 +11,7 @@ export const hasNoAccessError = state => state.errorCode === httpStatus.FORBIDDE
export const currentValueStreamId = ({ selectedValueStream }) => export const currentValueStreamId = ({ selectedValueStream }) =>
selectedValueStream?.id || DEFAULT_VALUE_STREAM_ID; selectedValueStream?.id || DEFAULT_VALUE_STREAM_ID;
export const currentGroupPath = ({ selectedGroup }) => selectedGroup?.fullPath || null; export const currentGroupPath = ({ currentGroup }) => currentGroup?.fullPath || null;
export const selectedProjectIds = ({ selectedProjects }) => export const selectedProjectIds = ({ selectedProjects }) =>
selectedProjects?.map(({ id }) => id) || []; selectedProjects?.map(({ id }) => id) || [];
......
...@@ -2,9 +2,9 @@ import { getTasksByTypeData } from '../../../utils'; ...@@ -2,9 +2,9 @@ import { getTasksByTypeData } from '../../../utils';
export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => { export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => {
const { selectedLabelIds = [], subject } = state; const { selectedLabelIds = [], subject } = state;
const { selectedGroup, selectedProjectIds = [], startDate = null, endDate = null } = rootState; const { currentGroup, selectedProjectIds = [], startDate = null, endDate = null } = rootState;
return { return {
selectedGroup, currentGroup,
selectedProjectIds, selectedProjectIds,
startDate, startDate,
endDate, endDate,
......
export const SET_FEATURE_FLAGS = 'SET_FEATURE_FLAGS'; export const SET_FEATURE_FLAGS = 'SET_FEATURE_FLAGS';
export const SET_SELECTED_GROUP = 'SET_SELECTED_GROUP';
export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS'; export const SET_SELECTED_PROJECTS = 'SET_SELECTED_PROJECTS';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE'; export const SET_DATE_RANGE = 'SET_DATE_RANGE';
......
...@@ -6,10 +6,6 @@ export default { ...@@ -6,10 +6,6 @@ export default {
[types.SET_FEATURE_FLAGS](state, featureFlags) { [types.SET_FEATURE_FLAGS](state, featureFlags) {
state.featureFlags = featureFlags; state.featureFlags = featureFlags;
}, },
[types.SET_SELECTED_GROUP](state, group) {
state.selectedGroup = convertObjectPropsToCamelCase(group, { deep: true });
state.selectedProjects = [];
},
[types.SET_SELECTED_PROJECTS](state, projects) { [types.SET_SELECTED_PROJECTS](state, projects) {
state.selectedProjects = projects; state.selectedProjects = projects;
}, },
...@@ -91,14 +87,14 @@ export default { ...@@ -91,14 +87,14 @@ export default {
[types.INITIALIZE_CYCLE_ANALYTICS]( [types.INITIALIZE_CYCLE_ANALYTICS](
state, state,
{ {
group: selectedGroup = null, group = null,
createdAfter: startDate = null, createdAfter: startDate = null,
createdBefore: endDate = null, createdBefore: endDate = null,
selectedProjects = [], selectedProjects = [],
} = {}, } = {},
) { ) {
state.isLoading = true; state.isLoading = true;
state.selectedGroup = selectedGroup; state.currentGroup = group;
state.selectedProjects = selectedProjects; state.selectedProjects = selectedProjects;
state.startDate = startDate; state.startDate = startDate;
state.endDate = endDate; state.endDate = endDate;
......
...@@ -13,7 +13,7 @@ export default () => ({ ...@@ -13,7 +13,7 @@ export default () => ({
isSavingStageOrder: false, isSavingStageOrder: false,
errorSavingStageOrder: false, errorSavingStageOrder: false,
selectedGroup: null, currentGroup: null,
selectedProjects: [], selectedProjects: [],
selectedStage: null, selectedStage: null,
selectedValueStream: null, selectedValueStream: null,
......
import initCycleAnalyticsApp from 'ee/analytics/cycle_analytics/index';
document.addEventListener('DOMContentLoaded', initCycleAnalyticsApp);
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
- data_attributes = @request_params.valid? ? @request_params.to_data_attributes : {} - 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) } : {} - 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")} - 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")}
- settings = { hide_group_drop_down: 'true' } - data_attributes.merge!(api_paths, image_paths)
- data_attributes.merge!(api_paths, image_paths, settings)
#js-cycle-analytics-app{ data: data_attributes } #js-cycle-analytics-app{ data: data_attributes }
...@@ -77,7 +77,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -77,7 +77,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
it 'displays empty text' do it 'displays empty text' do
[ [
'Value Stream Analytics can help you determine your team’s velocity', 'Value Stream Analytics can help you determine your team’s velocity',
'Start by choosing a group to see how your team is spending time. You can then drill down to the project level.' 'Filter parameters are not valid. Make sure that the end date is after the start date.'
].each do |content| ].each do |content|
expect(page).to have_content(content) expect(page).to have_content(content)
end end
......
import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import store from 'ee/analytics/cycle_analytics/store'; import createStore 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';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import GroupsDropdownFilter from 'ee/analytics/shared/components/groups_dropdown_filter.vue';
import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from 'ee/analytics/shared/components/projects_dropdown_filter.vue';
import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue'; import Metrics from 'ee/analytics/cycle_analytics/components/metrics.vue';
import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue'; import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue';
...@@ -30,9 +29,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; ...@@ -30,9 +29,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access'; const noAccessSvgPath = 'path/to/no/access';
const currentGroup = convertObjectPropsToCamelCase(mockData.group);
const emptyStateSvgPath = 'path/to/empty/state'; const emptyStateSvgPath = 'path/to/empty/state';
const hideGroupDropDown = false;
const selectedGroup = convertObjectPropsToCamelCase(mockData.group);
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -43,7 +41,6 @@ const defaultStubs = { ...@@ -43,7 +41,6 @@ const defaultStubs = {
'tasks-by-type-chart': true, 'tasks-by-type-chart': true,
'labels-selector': true, 'labels-selector': true,
DurationChart: true, DurationChart: true,
GroupsDropdownFilter: true,
ValueStreamSelect: true, ValueStreamSelect: true,
Metrics: true, Metrics: true,
UrlSync, UrlSync,
...@@ -58,7 +55,7 @@ const defaultFeatureFlags = { ...@@ -58,7 +55,7 @@ const defaultFeatureFlags = {
const initialCycleAnalyticsState = { const initialCycleAnalyticsState = {
createdAfter: mockData.startDate, createdAfter: mockData.startDate,
createdBefore: mockData.endDate, createdBefore: mockData.endDate,
group: selectedGroup, group: currentGroup,
}; };
const mocks = { const mocks = {
...@@ -67,57 +64,21 @@ const mocks = { ...@@ -67,57 +64,21 @@ const mocks = {
}, },
}; };
function createComponent({ function mockRequiredRoutes(mockAdapter) {
opts = { mockAdapter.onGet(mockData.endpoints.stageData).reply(httpStatusCodes.OK, mockData.issueEvents);
stubs: defaultStubs, mockAdapter
}, .onGet(mockData.endpoints.tasksByTypeTopLabelsData)
shallow = true, .reply(httpStatusCodes.OK, mockData.groupLabels);
withStageSelected = false, mockAdapter
withValueStreamSelected = true, .onGet(mockData.endpoints.tasksByTypeData)
featureFlags = {}, .reply(httpStatusCodes.OK, { ...mockData.tasksByTypeData });
props = {}, mockAdapter
} = {}) { .onGet(mockData.endpoints.baseStagesEndpoint)
const func = shallow ? shallowMount : mount; .reply(httpStatusCodes.OK, { ...mockData.customizableStagesAndEvents });
const comp = func(Component, { mockAdapter
localVue, .onGet(mockData.endpoints.durationData)
store, .reply(httpStatusCodes.OK, mockData.customizableStagesAndEvents.stages);
propsData: { mockAdapter.onGet(mockData.endpoints.stageMedian).reply(httpStatusCodes.OK, { value: null });
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
hideGroupDropDown,
...props,
},
mocks,
...opts,
});
comp.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
featureFlags: {
...defaultFeatureFlags,
...featureFlags,
},
});
if (withValueStreamSelected) {
comp.vm.$store.dispatch('receiveValueStreamsSuccess', mockData.valueStreams);
}
if (withStageSelected) {
comp.vm.$store.commit('SET_SELECTED_GROUP', {
...selectedGroup,
});
comp.vm.$store.dispatch(
'receiveGroupStagesSuccess',
mockData.customizableStagesAndEvents.stages,
);
comp.vm.$store.dispatch('receiveStageDataSuccess', mockData.issueEvents);
}
return comp;
} }
async function shouldMergeUrlParams(wrapper, result) { async function shouldMergeUrlParams(wrapper, result) {
...@@ -131,6 +92,56 @@ async function shouldMergeUrlParams(wrapper, result) { ...@@ -131,6 +92,56 @@ async function shouldMergeUrlParams(wrapper, result) {
describe('Cycle Analytics component', () => { describe('Cycle Analytics component', () => {
let wrapper; let wrapper;
let mock; let mock;
let store;
async function createComponent(options = {}) {
const {
opts = {
stubs: defaultStubs,
},
shallow = true,
withStageSelected = false,
withValueStreamSelected = true,
featureFlags = {},
initialState = initialCycleAnalyticsState,
props = {},
} = options;
store = createStore();
await store.dispatch('initializeCycleAnalytics', {
...initialState,
featureFlags: {
...defaultFeatureFlags,
...featureFlags,
},
});
const func = shallow ? shallowMount : mount;
const comp = func(Component, {
localVue,
store,
propsData: {
emptyStateSvgPath,
noDataSvgPath,
noAccessSvgPath,
...props,
},
mocks,
...opts,
});
if (withValueStreamSelected) {
await store.dispatch('receiveValueStreamsSuccess', mockData.valueStreams);
}
if (withStageSelected) {
await Promise.all([
store.dispatch('receiveGroupStagesSuccess', mockData.customizableStagesAndEvents.stages),
store.dispatch('receiveStageDataSuccess', mockData.issueEvents),
]);
}
return comp;
}
const findStageNavItemAtIndex = index => const findStageNavItemAtIndex = index =>
wrapper wrapper
...@@ -180,23 +191,25 @@ describe('Cycle Analytics component', () => { ...@@ -180,23 +191,25 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(ValueStreamSelect).exists()).toBe(flag); expect(wrapper.find(ValueStreamSelect).exists()).toBe(flag);
}; };
beforeEach(() => { describe('displays the components as required', () => {
mock = new MockAdapter(axios); describe('without a group', () => {
wrapper = createComponent({ beforeEach(async () => {
featureFlags: { const { group, ...stateWithoutGroup } = initialCycleAnalyticsState;
hasPathNavigation: true, mock = new MockAdapter(axios);
}, wrapper = await createComponent({
}); featureFlags: {
}); hasPathNavigation: true,
},
initialState: stateWithoutGroup,
});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore(); mock.restore();
wrapper = null; wrapper = null;
}); });
describe('displays the components as required', () => {
describe('before a filter has been selected', () => {
it('displays an empty state', () => { it('displays an empty state', () => {
const emptyState = wrapper.find(GlEmptyState); const emptyState = wrapper.find(GlEmptyState);
...@@ -204,13 +217,6 @@ describe('Cycle Analytics component', () => { ...@@ -204,13 +217,6 @@ describe('Cycle Analytics component', () => {
expect(emptyState.props('svgPath')).toBe(emptyStateSvgPath); expect(emptyState.props('svgPath')).toBe(emptyStateSvgPath);
}); });
it('displays the groups filter', () => {
expect(wrapper.find(GroupsDropdownFilter).exists()).toBe(true);
expect(wrapper.find(GroupsDropdownFilter).props('queryParams')).toEqual(
wrapper.vm.$options.groupsQueryParams,
);
});
it('does not display the projects filter', () => { it('does not display the projects filter', () => {
displaysProjectsDropdownFilter(false); displaysProjectsDropdownFilter(false);
}); });
...@@ -242,43 +248,30 @@ describe('Cycle Analytics component', () => { ...@@ -242,43 +248,30 @@ describe('Cycle Analytics component', () => {
it('does not display the value stream select component', () => { it('does not display the value stream select component', () => {
displaysValueStreamSelect(false); displaysValueStreamSelect(false);
}); });
});
describe('hideGroupDropDown = true', () => { describe('with a group', () => {
beforeEach(() => { beforeEach(async () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ mockRequiredRoutes(mock);
props: { wrapper = await createComponent({
hideGroupDropDown: true, featureFlags: {
}, hasPathNavigation: true,
}); },
});
it('does not render the group dropdown', () => {
expect(wrapper.find(GroupsDropdownFilter).exists()).toBe(false);
}); });
}); });
describe('hasCreateMultipleValueStreams = true', () => { afterEach(() => {
beforeEach(() => { wrapper.destroy();
mock = new MockAdapter(axios); mock.restore();
wrapper = createComponent({ wrapper = null;
featureFlags: {
hasCreateMultipleValueStreams: true,
},
});
});
it('displays the value stream select component', () => {
displaysValueStreamSelect(true);
});
}); });
});
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(async () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent({ mockRequiredRoutes(mock);
wrapper = await createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: { featureFlags: {
hasPathNavigation: true, hasPathNavigation: true,
...@@ -296,15 +289,35 @@ describe('Cycle Analytics component', () => { ...@@ -296,15 +289,35 @@ describe('Cycle Analytics component', () => {
expect(wrapper.find(ProjectsDropdownFilter).props()).toEqual( expect(wrapper.find(ProjectsDropdownFilter).props()).toEqual(
expect.objectContaining({ expect.objectContaining({
queryParams: wrapper.vm.projectsQueryParams, queryParams: wrapper.vm.projectsQueryParams,
groupId: mockData.group.id,
multiSelect: wrapper.vm.$options.multiProjectSelect, multiSelect: wrapper.vm.$options.multiProjectSelect,
}), }),
); );
}); });
describe('when analyticsSimilaritySearch feature flag is on', () => { describe('hasCreateMultipleValueStreams = true', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
});
it('hides the value stream select component', () => {
displaysValueStreamSelect(false);
});
it('displays the value stream select component', async () => {
wrapper = await createComponent({
featureFlags: {
hasCreateMultipleValueStreams: true,
},
});
displaysValueStreamSelect(true);
});
});
describe('when analyticsSimilaritySearch feature flag is on', () => {
beforeEach(async () => {
wrapper = await createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: { featureFlags: {
hasAnalyticsSimilaritySearch: true, hasAnalyticsSimilaritySearch: true,
...@@ -337,27 +350,26 @@ describe('Cycle Analytics component', () => { ...@@ -337,27 +350,26 @@ describe('Cycle Analytics component', () => {
displaysFilterBar(true); displaysFilterBar(true);
}); });
it('displays the add stage button', () => { it('displays the add stage button', async () => {
wrapper = createComponent({ wrapper = await createComponent({
opts: { opts: {
stubs: { stubs: {
StageTable, StageTable,
StageTableNav, StageTableNav,
AddStageButton,
}, },
}, },
withStageSelected: true, withStageSelected: true,
}); });
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
displaysAddStageButton(true); displaysAddStageButton(true);
});
}); });
it('displays the tasks by type chart', () => { it('displays the tasks by type chart', async () => {
wrapper = createComponent({ shallow: false, withStageSelected: true }); wrapper = await createComponent({ shallow: false, withStageSelected: true });
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.find('.js-tasks-by-type-chart').exists()).toBe(true); expect(wrapper.find('.js-tasks-by-type-chart').exists()).toBe(true);
});
}); });
it('displays the duration chart', () => { it('displays the duration chart', () => {
...@@ -366,8 +378,8 @@ describe('Cycle Analytics component', () => { ...@@ -366,8 +378,8 @@ describe('Cycle Analytics component', () => {
describe('path navigation', () => { describe('path navigation', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = createComponent({ wrapper = await createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: { featureFlags: {
hasPathNavigation: false, hasPathNavigation: false,
...@@ -381,8 +393,8 @@ describe('Cycle Analytics component', () => { ...@@ -381,8 +393,8 @@ describe('Cycle Analytics component', () => {
}); });
describe('enabled', () => { describe('enabled', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = createComponent({ wrapper = await createComponent({
withStageSelected: true, withStageSelected: true,
featureFlags: { featureFlags: {
hasPathNavigation: true, hasPathNavigation: true,
...@@ -397,10 +409,11 @@ describe('Cycle Analytics component', () => { ...@@ -397,10 +409,11 @@ describe('Cycle Analytics component', () => {
}); });
describe('StageTable', () => { describe('StageTable', () => {
beforeEach(() => { beforeEach(async () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockRequiredRoutes(mock);
wrapper = createComponent({ wrapper = await createComponent({
opts: { opts: {
stubs: { stubs: {
StageTable, StageTable,
...@@ -413,7 +426,7 @@ describe('Cycle Analytics component', () => { ...@@ -413,7 +426,7 @@ describe('Cycle Analytics component', () => {
}); });
}); });
it('has the first stage selected by default', () => { it('has the first stage selected by default', async () => {
const first = findStageNavItemAtIndex(0); const first = findStageNavItemAtIndex(0);
const second = findStageNavItemAtIndex(1); const second = findStageNavItemAtIndex(1);
...@@ -421,21 +434,19 @@ describe('Cycle Analytics component', () => { ...@@ -421,21 +434,19 @@ describe('Cycle Analytics component', () => {
expect(second.props('isActive')).toBe(false); expect(second.props('isActive')).toBe(false);
}); });
it('can navigate to different stages', () => { it('can navigate to different stages', async () => {
findStageNavItemAtIndex(2).trigger('click'); findStageNavItemAtIndex(2).trigger('click');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
const first = findStageNavItemAtIndex(0); const first = findStageNavItemAtIndex(0);
const third = findStageNavItemAtIndex(2); const third = findStageNavItemAtIndex(2);
expect(third.props('isActive')).toBe(true);
expect(third.props('isActive')).toBe(true); expect(first.props('isActive')).toBe(false);
expect(first.props('isActive')).toBe(false);
});
}); });
describe('Add stage button', () => { describe('Add stage button', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = createComponent({ wrapper = await createComponent({
opts: { opts: {
stubs: { stubs: {
StageTable, StageTable,
...@@ -447,87 +458,87 @@ describe('Cycle Analytics component', () => { ...@@ -447,87 +458,87 @@ describe('Cycle Analytics component', () => {
}); });
}); });
it('can navigate to the custom stage form', () => { it('can navigate to the custom stage form', async () => {
expect(wrapper.find(CustomStageForm).exists()).toBe(false); expect(wrapper.find(CustomStageForm).exists()).toBe(false);
findAddStageButton().trigger('click'); findAddStageButton().trigger('click');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.find(CustomStageForm).exists()).toBe(true); expect(wrapper.find(CustomStageForm).exists()).toBe(true);
});
}); });
}); });
}); });
});
describe('the user does not have access to the group', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onAny().reply(httpStatusCodes.FORBIDDEN);
wrapper.vm.onGroupSelect(mockData.group);
return waitForPromises();
});
it('renders the no access information', () => { describe('the user does not have access to the group', () => {
const emptyState = wrapper.find(GlEmptyState); beforeEach(async () => {
await store.dispatch('receiveCycleAnalyticsDataError', {
response: { status: httpStatusCodes.FORBIDDEN },
});
});
expect(emptyState.exists()).toBe(true); it('renders the no access information', () => {
expect(emptyState.props('svgPath')).toBe(noAccessSvgPath); const emptyState = wrapper.find(GlEmptyState);
});
it('does not display the projects filter', () => { expect(emptyState.exists()).toBe(true);
displaysProjectsDropdownFilter(false); expect(emptyState.props('svgPath')).toBe(noAccessSvgPath);
}); });
it('does not display the date range picker', () => { it('does not display the projects filter', () => {
displaysDateRangePicker(false); displaysProjectsDropdownFilter(false);
}); });
it('does not display the metrics', () => { it('does not display the date range picker', () => {
displaysMetrics(false); displaysDateRangePicker(false);
}); });
it('does not display the stage table', () => { it('does not display the metrics', () => {
displaysStageTable(false); displaysMetrics(false);
}); });
it('does not display the add stage button', () => { it('does not display the stage table', () => {
displaysAddStageButton(false); displaysStageTable(false);
}); });
it('does not display the tasks by type chart', () => { it('does not display the add stage button', () => {
displaysTypeOfWork(false); displaysAddStageButton(false);
}); });
it('does not display the duration chart', () => { it('does not display the tasks by type chart', () => {
displaysDurationChart(false); displaysTypeOfWork(false);
}); });
describe('path navigation', () => { it('does not display the duration chart', () => {
describe('disabled', () => { displaysDurationChart(false);
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
}); });
describe('enabled', () => { describe('path navigation', () => {
beforeEach(() => { describe('disabled', () => {
wrapper = createComponent({ it('does not display the path navigation', () => {
withValueStreamSelected: false, displaysPathNavigation(false);
withStageSelected: true,
pathNavigationEnabled: true,
}); });
});
mock = new MockAdapter(axios); describe('enabled', () => {
mock.onAny().reply(httpStatusCodes.FORBIDDEN); beforeEach(async () => {
wrapper = await createComponent({
withValueStreamSelected: false,
withStageSelected: true,
pathNavigationEnabled: true,
});
wrapper.vm.onGroupSelect(mockData.group); mock = new MockAdapter(axios);
return waitForPromises(); mockRequiredRoutes(mock);
}); mock.onAny().reply(httpStatusCodes.FORBIDDEN);
it('does not display the path navigation', () => { await waitForPromises();
displaysPathNavigation(false); });
afterEach(() => {
mock.restore();
});
it('does not display the path navigation', () => {
displaysPathNavigation(false);
});
}); });
}); });
}); });
...@@ -536,215 +547,123 @@ describe('Cycle Analytics component', () => { ...@@ -536,215 +547,123 @@ describe('Cycle Analytics component', () => {
}); });
describe('with failed requests while loading', () => { describe('with failed requests while loading', () => {
const mockRequestCycleAnalyticsData = ({ beforeEach(async () => {
overrides = {},
mockFetchStageData = true,
mockFetchStageMedian = true,
mockFetchTasksByTypeData = true,
mockFetchTopRankedGroupLabels = true,
}) => {
const defaultStatus = 200;
const defaultRequests = {
fetchGroupStagesAndEvents: {
status: defaultStatus,
endpoint: mockData.endpoints.baseStagesEndpoint,
response: { ...mockData.customizableStagesAndEvents },
},
...overrides,
};
if (mockFetchTopRankedGroupLabels) {
mock
.onGet(mockData.endpoints.tasksByTypeTopLabelsData)
.reply(defaultStatus, mockData.groupLabels);
}
if (mockFetchTasksByTypeData) {
mock
.onGet(mockData.endpoints.tasksByTypeData)
.reply(defaultStatus, { ...mockData.tasksByTypeData });
}
if (mockFetchStageMedian) {
mock.onGet(mockData.endpoints.stageMedian).reply(defaultStatus, { value: null });
}
if (mockFetchStageData) {
mock.onGet(mockData.endpoints.stageData).reply(defaultStatus, mockData.issueEvents);
}
Object.values(defaultRequests).forEach(({ endpoint, status, response }) => {
mock.onGet(endpoint).replyOnce(status, response);
});
};
beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent(); mockRequiredRoutes(mock);
wrapper = await createComponent({
featureFlags: {
hasPathNavigation: true,
},
});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore(); mock.restore();
wrapper = null;
}); });
const findFlashError = () => document.querySelector('.flash-container .flash-text'); const findFlashError = () => document.querySelector('.flash-container .flash-text');
const selectGroupAndFindError = msg => { const findError = async msg => {
wrapper.vm.onGroupSelect(mockData.group); await waitForPromises();
expect(findFlashError().innerText.trim()).toEqual(msg);
return waitForPromises().then(() => {
expect(findFlashError().innerText.trim()).toEqual(msg);
});
}; };
it('will display an error if the fetchGroupStagesAndEvents request fails', () => { it('will display an error if the fetchGroupStagesAndEvents request fails', async () => {
expect(findFlashError()).toBeNull(); expect(await findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({ mock
overrides: { .onGet(mockData.endpoints.baseStagesEndpoint)
fetchGroupStagesAndEvents: { .reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
endPoint: mockData.endpoints.baseStagesEndpoint, wrapper = await createComponent();
status: httpStatusCodes.NOT_FOUND,
response: { response: { status: httpStatusCodes.NOT_FOUND } },
},
},
});
return selectGroupAndFindError('There was an error fetching value stream analytics stages.'); await findError('There was an error fetching value stream analytics stages.');
}); });
it('will display an error if the fetchStageData request fails', () => { it('will display an error if the fetchStageData request fails', async () => {
expect(findFlashError()).toBeNull(); expect(await findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({ mock
mockFetchStageData: false, .onGet(mockData.endpoints.stageData)
}); .reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
return selectGroupAndFindError('There was an error fetching data for the selected stage'); await findError('There was an error fetching data for the selected stage');
}); });
it('will display an error if the fetchTopRankedGroupLabels request fails', () => { it('will display an error if the fetchTopRankedGroupLabels request fails', async () => {
expect(findFlashError()).toBeNull(); expect(await findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({ mockFetchTopRankedGroupLabels: false }); mock
.onGet(mockData.endpoints.tasksByTypeTopLabelsData)
.reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
return selectGroupAndFindError( await findError('There was an error fetching the top labels for the selected group');
'There was an error fetching the top labels for the selected group',
);
}); });
it('will display an error if the fetchTasksByTypeData request fails', () => { it('will display an error if the fetchTasksByTypeData request fails', async () => {
expect(findFlashError()).toBeNull(); expect(await findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({ mockFetchTasksByTypeData: false }); mock
.onGet(mockData.endpoints.tasksByTypeData)
.reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
return selectGroupAndFindError( await findError('There was an error fetching data for the tasks by type chart');
'There was an error fetching data for the tasks by type chart',
);
}); });
it('will display an error if the fetchStageMedian request fails', () => { it('will display an error if the fetchStageMedian request fails', async () => {
expect(findFlashError()).toBeNull(); expect(await findFlashError()).toBeNull();
mockRequestCycleAnalyticsData({
mockFetchStageMedian: false,
});
wrapper.vm.onGroupSelect(mockData.group); mock
.onGet(mockData.endpoints.stageMedian)
.reply(httpStatusCodes.NOT_FOUND, { response: { status: httpStatusCodes.NOT_FOUND } });
await createComponent();
return waitForPromises().catch(() => { await waitForPromises();
expect(findFlashError().innerText.trim()).toEqual( expect(await findFlashError().innerText.trim()).toEqual(
'There was an error while fetching value stream analytics data.', 'There was an error fetching median data for stages',
); );
});
}); });
}); });
describe('Url parameters', () => { describe('Url parameters', () => {
const fakeGroup = {
id: 2,
path: 'new-test',
fullPath: 'new-test-group',
name: 'New test group',
};
const defaultParams = { const defaultParams = {
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
project_ids: null, project_ids: null,
}; };
const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id); const selectedProjectIds = mockData.selectedProjects.map(({ id }) => id);
beforeEach(() => { beforeEach(async () => {
commonUtils.historyPushState = jest.fn(); commonUtils.historyPushState = jest.fn();
urlUtils.mergeUrlParams = jest.fn(); urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent(); mockRequiredRoutes(mock);
wrapper = await createComponent();
wrapper.vm.$store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState); await store.dispatch('initializeCycleAnalytics', initialCycleAnalyticsState);
}); });
it('sets the created_after and created_before url parameters', async () => { afterEach(() => {
await shouldMergeUrlParams(wrapper, defaultParams); wrapper.destroy();
}); mock.restore();
wrapper = null;
describe('with hideGroupDropDown=true', () => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.mergeUrlParams = jest.fn();
mock = new MockAdapter(axios);
wrapper = createComponent({
props: {
hideGroupDropDown: true,
},
});
wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
...initialCycleAnalyticsState,
group: fakeGroup,
});
});
it('sets the group_id url parameter', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: null,
});
});
}); });
describe('with a group selected', () => { it('sets the created_after and created_before url parameters', async () => {
beforeEach(() => { await shouldMergeUrlParams(wrapper, defaultParams);
wrapper.vm.$store.dispatch('setSelectedGroup', {
...fakeGroup,
});
});
it('sets the group_id url parameter', async () => {
await shouldMergeUrlParams(wrapper, {
...defaultParams,
group_id: fakeGroup.fullPath,
});
});
}); });
describe('with a group and selectedProjectIds set', () => { describe('with selectedProjectIds set', () => {
beforeEach(() => { beforeEach(async () => {
wrapper.vm.$store.dispatch('setSelectedGroup', { store.dispatch('setSelectedProjects', mockData.selectedProjects);
...selectedGroup, await wrapper.vm.$nextTick();
});
wrapper.vm.$store.dispatch('setSelectedProjects', mockData.selectedProjects);
return wrapper.vm.$nextTick();
}); });
it('sets the project_ids url parameter', async () => { it('sets the project_ids url parameter', async () => {
...@@ -752,7 +671,6 @@ describe('Cycle Analytics component', () => { ...@@ -752,7 +671,6 @@ describe('Cycle Analytics component', () => {
...defaultParams, ...defaultParams,
created_after: toYmd(mockData.startDate), created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate), created_before: toYmd(mockData.endDate),
group_id: selectedGroup.fullPath,
project_ids: selectedProjectIds, project_ids: selectedProjectIds,
}); });
}); });
......
...@@ -52,7 +52,7 @@ export const group = { ...@@ -52,7 +52,7 @@ export const group = {
avatar_url: `${TEST_HOST}/images/home/nasa.svg`, avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
}; };
export const selectedGroup = convertObjectPropsToCamelCase(group, { deep: true }); export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true });
const getStageByTitle = (stages, title) => const getStageByTitle = (stages, title) =>
stages.find(stage => stage.title && stage.title.toLowerCase().trim() === title) || {}; stages.find(stage => stage.title && stage.title.toLowerCase().trim() === title) || {};
...@@ -189,7 +189,7 @@ export const tasksByTypeData = { ...@@ -189,7 +189,7 @@ export const tasksByTypeData = {
}; };
export const taskByTypeFilters = { export const taskByTypeFilters = {
selectedGroup: { currentGroup: {
id: 22, id: 22,
name: 'Gitlab Org', name: 'Gitlab Org',
fullName: 'Gitlab Org', fullName: 'Gitlab Org',
......
...@@ -7,7 +7,7 @@ import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; ...@@ -7,7 +7,7 @@ import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { import {
selectedGroup, currentGroup,
allowedStages as stages, allowedStages as stages,
startDate, startDate,
endDate, endDate,
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
valueStreams, valueStreams,
} from '../mock_data'; } from '../mock_data';
const group = { parentId: 'fake_group_parent_id', fullPath: 'fake_group_full_path' }; const group = { fullPath: 'fake_group_full_path' };
const milestonesPath = 'fake_milestones_path'; const milestonesPath = 'fake_milestones_path';
const labelsPath = 'fake_labels_path'; const labelsPath = 'fake_labels_path';
...@@ -33,12 +33,12 @@ const selectedStageSlug = selectedStage.slug; ...@@ -33,12 +33,12 @@ const selectedStageSlug = selectedStage.slug;
const [selectedValueStream] = valueStreams; const [selectedValueStream] = valueStreams;
const mockGetters = { const mockGetters = {
currentGroupPath: () => selectedGroup.fullPath, currentGroupPath: () => currentGroup.fullPath,
currentValueStreamId: () => selectedValueStream.id, currentValueStreamId: () => selectedValueStream.id,
}; };
const stageEndpoint = ({ stageId }) => const stageEndpoint = ({ stageId }) =>
`/groups/${selectedGroup.fullPath}/-/analytics/value_stream_analytics/value_streams/${selectedValueStream.id}/stages/${stageId}`; `/groups/${currentGroup.fullPath}/-/analytics/value_stream_analytics/value_streams/${selectedValueStream.id}/stages/${stageId}`;
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -68,7 +68,7 @@ describe('Cycle analytics actions', () => { ...@@ -68,7 +68,7 @@ describe('Cycle analytics actions', () => {
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
state = { ...state, selectedGroup: null }; state = { ...state, currentGroup: null };
}); });
it.each` it.each`
...@@ -106,87 +106,22 @@ describe('Cycle analytics actions', () => { ...@@ -106,87 +106,22 @@ describe('Cycle analytics actions', () => {
}); });
describe('setPaths', () => { describe('setPaths', () => {
describe('with endpoint paths provided', () => { it('dispatches the filters/setEndpoints action with enpoints', () => {
it('dispatches the filters/setEndpoints action with enpoints', () => { return testAction(
return testAction( actions.setPaths,
actions.setPaths, { groupPath: group.fullPath, milestonesPath, labelsPath },
{ group, milestonesPath, labelsPath }, state,
state, [],
[], [
[ {
{ type: 'filters/setEndpoints',
type: 'filters/setEndpoints', payload: {
payload: { groupEndpoint: 'fake_group_full_path',
groupEndpoint: 'fake_group_parent_id', labelsEndpoint: 'fake_labels_path.json',
labelsEndpoint: 'fake_labels_path.json', milestonesEndpoint: 'fake_milestones_path.json',
milestonesEndpoint: 'fake_milestones_path.json',
},
},
],
);
});
});
describe('without endpoint paths provided', () => {
it('dispatches the filters/setEndpoints action and prefers group.parentId', () => {
return testAction(
actions.setPaths,
{ group },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: 'fake_group_parent_id',
labelsEndpoint: '/groups/fake_group_parent_id/-/labels.json',
milestonesEndpoint: '/groups/fake_group_parent_id/-/milestones.json',
},
},
],
);
});
it('dispatches the filters/setEndpoints action and uses group.fullPath', () => {
const { fullPath } = group;
return testAction(
actions.setPaths,
{ group: { fullPath } },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: 'fake_group_full_path',
labelsEndpoint: '/groups/fake_group_full_path/-/labels.json',
milestonesEndpoint: '/groups/fake_group_full_path/-/milestones.json',
},
}, },
], },
); ],
});
it.each([undefined, null, { parentId: null }, { fullPath: null }, {}])(
'group=%s will return empty string',
value => {
return testAction(
actions.setPaths,
{ group: value, milestonesPath, labelsPath },
state,
[],
[
{
type: 'filters/setEndpoints',
payload: {
groupEndpoint: '',
labelsEndpoint: 'fake_labels_path.json',
milestonesEndpoint: 'fake_milestones_path.json',
},
},
],
);
},
); );
}); });
}); });
...@@ -205,34 +140,9 @@ describe('Cycle analytics actions', () => { ...@@ -205,34 +140,9 @@ describe('Cycle analytics actions', () => {
}); });
}); });
describe('setSelectedGroup', () => {
const { fullPath } = selectedGroup;
beforeEach(() => {
mock = new MockAdapter(axios);
});
it('commits the setSelectedGroup mutation', () => {
return testAction(
actions.setSelectedGroup,
{ full_path: fullPath },
state,
[{ type: types.SET_SELECTED_GROUP, payload: { full_path: fullPath } }],
[
{
type: 'filters/initialize',
payload: {
groupPath: fullPath,
},
},
],
);
});
});
describe('fetchStageData', () => { describe('fetchStageData', () => {
beforeEach(() => { beforeEach(() => {
state = { ...state, selectedGroup }; state = { ...state, currentGroup };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(endpoints.stageData).reply(httpStatusCodes.OK, { events: [] }); mock.onGet(endpoints.stageData).reply(httpStatusCodes.OK, { events: [] });
}); });
...@@ -338,7 +248,7 @@ describe('Cycle analytics actions', () => { ...@@ -338,7 +248,7 @@ describe('Cycle analytics actions', () => {
} }
beforeEach(() => { beforeEach(() => {
state = { ...state, selectedGroup, startDate, endDate }; state = { ...state, currentGroup, startDate, endDate };
}); });
it(`dispatches actions for required value stream analytics analytics data`, () => { it(`dispatches actions for required value stream analytics analytics data`, () => {
...@@ -713,7 +623,7 @@ describe('Cycle analytics actions', () => { ...@@ -713,7 +623,7 @@ describe('Cycle analytics actions', () => {
beforeEach(() => { beforeEach(() => {
mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.OK); mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.OK);
state = { selectedGroup }; state = { currentGroup };
}); });
it('dispatches fetchCycleAnalyticsData', () => { it('dispatches fetchCycleAnalyticsData', () => {
...@@ -745,7 +655,7 @@ describe('Cycle analytics actions', () => { ...@@ -745,7 +655,7 @@ describe('Cycle analytics actions', () => {
const fetchMedianResponse = activeStages.map(({ slug: id }) => ({ events: [], id })); const fetchMedianResponse = activeStages.map(({ slug: id }) => ({ events: [], id }));
beforeEach(() => { beforeEach(() => {
state = { ...state, stages, selectedGroup }; state = { ...state, stages, currentGroup };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.OK, { events: [] }); mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.OK, { events: [] });
mockDispatch = jest.fn(); mockDispatch = jest.fn();
...@@ -846,7 +756,7 @@ describe('Cycle analytics actions', () => { ...@@ -846,7 +756,7 @@ describe('Cycle analytics actions', () => {
let store; let store;
const initialData = { const initialData = {
group: selectedGroup, group: currentGroup,
projectIds: [1, 2], projectIds: [1, 2],
}; };
...@@ -861,30 +771,29 @@ describe('Cycle analytics actions', () => { ...@@ -861,30 +771,29 @@ describe('Cycle analytics actions', () => {
}; };
}); });
describe('with no initialData', () => { describe('with only group in initialData', () => {
it('commits "INITIALIZE_CYCLE_ANALYTICS"', () => it('commits "INITIALIZE_CYCLE_ANALYTICS"', async () => {
actions.initializeCycleAnalytics(store).then(() => { await actions.initializeCycleAnalytics(store, { group });
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_CYCLE_ANALYTICS', {}); expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_CYCLE_ANALYTICS', { group });
})); });
it('dispatches "initializeCycleAnalyticsSuccess"', () => it('dispatches "fetchCycleAnalyticsData" and "initializeCycleAnalyticsSuccess"', async () => {
actions.initializeCycleAnalytics(store).then(() => { await actions.initializeCycleAnalytics(store, { group });
expect(mockDispatch).not.toHaveBeenCalledWith('fetchCycleAnalyticsData'); expect(mockDispatch).toHaveBeenCalledWith('fetchCycleAnalyticsData');
expect(mockDispatch).toHaveBeenCalledWith('initializeCycleAnalyticsSuccess'); });
}));
}); });
describe('with initialData', () => { describe('with initialData', () => {
it('dispatches "fetchCycleAnalyticsData" and "initializeCycleAnalyticsSuccess"', () => it('dispatches "fetchCycleAnalyticsData" and "initializeCycleAnalyticsSuccess"', async () => {
actions.initializeCycleAnalytics(store, initialData).then(() => { await actions.initializeCycleAnalytics(store, initialData);
expect(mockDispatch).toHaveBeenCalledWith('fetchCycleAnalyticsData'); expect(mockDispatch).toHaveBeenCalledWith('fetchCycleAnalyticsData');
expect(mockDispatch).toHaveBeenCalledWith('initializeCycleAnalyticsSuccess'); expect(mockDispatch).toHaveBeenCalledWith('initializeCycleAnalyticsSuccess');
})); });
it('commits "INITIALIZE_CYCLE_ANALYTICS"', () => it('commits "INITIALIZE_CYCLE_ANALYTICS"', async () => {
actions.initializeCycleAnalytics(store, initialData).then(() => { await actions.initializeCycleAnalytics(store, initialData);
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_CYCLE_ANALYTICS', initialData); expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_CYCLE_ANALYTICS', initialData);
})); });
}); });
}); });
...@@ -977,7 +886,7 @@ describe('Cycle analytics actions', () => { ...@@ -977,7 +886,7 @@ describe('Cycle analytics actions', () => {
const payload = { name: 'cool value stream' }; const payload = { name: 'cool value stream' };
beforeEach(() => { beforeEach(() => {
state = { selectedGroup }; state = { currentGroup };
}); });
describe('with no errors', () => { describe('with no errors', () => {
...@@ -1030,7 +939,7 @@ describe('Cycle analytics actions', () => { ...@@ -1030,7 +939,7 @@ describe('Cycle analytics actions', () => {
const payload = 'my-fake-value-stream'; const payload = 'my-fake-value-stream';
beforeEach(() => { beforeEach(() => {
state = { selectedGroup }; state = { currentGroup };
}); });
describe('with no errors', () => { describe('with no errors', () => {
...@@ -1086,7 +995,7 @@ describe('Cycle analytics actions', () => { ...@@ -1086,7 +995,7 @@ describe('Cycle analytics actions', () => {
state = { state = {
...state, ...state,
stages: [{ slug: selectedStageSlug }], stages: [{ slug: selectedStageSlug }],
selectedGroup, currentGroup,
featureFlags: { featureFlags: {
...state.featureFlags, ...state.featureFlags,
hasCreateMultipleValueStreams: true, hasCreateMultipleValueStreams: true,
...@@ -1175,7 +1084,7 @@ describe('Cycle analytics actions', () => { ...@@ -1175,7 +1084,7 @@ describe('Cycle analytics actions', () => {
state = { state = {
...state, ...state,
stages: [{ slug: selectedStageSlug }], stages: [{ slug: selectedStageSlug }],
selectedGroup, currentGroup,
featureFlags: { featureFlags: {
...state.featureFlags, ...state.featureFlags,
hasCreateMultipleValueStreams: true, hasCreateMultipleValueStreams: true,
......
...@@ -63,11 +63,11 @@ describe('Cycle analytics getters', () => { ...@@ -63,11 +63,11 @@ describe('Cycle analytics getters', () => {
}); });
describe('currentGroupPath', () => { describe('currentGroupPath', () => {
describe('with selectedGroup set', () => { describe('with currentGroup set', () => {
it('returns the `fullPath` value of the group', () => { it('returns the `fullPath` value of the group', () => {
const fullPath = 'cool-beans'; const fullPath = 'cool-beans';
state = { state = {
selectedGroup: { currentGroup: {
fullPath, fullPath,
}, },
}; };
...@@ -76,9 +76,9 @@ describe('Cycle analytics getters', () => { ...@@ -76,9 +76,9 @@ describe('Cycle analytics getters', () => {
}); });
}); });
describe('without a selectedGroup set', () => { describe('without a currentGroup set', () => {
it.each([[''], [{}], [null]])('given "%s" will return null', value => { it.each([[''], [{}], [null]])('given "%s" will return null', value => {
state = { selectedGroup: value }; state = { currentGroup: value };
expect(getters.currentGroupPath(state)).toEqual(null); expect(getters.currentGroupPath(state)).toEqual(null);
}); });
}); });
...@@ -88,7 +88,7 @@ describe('Cycle analytics getters', () => { ...@@ -88,7 +88,7 @@ describe('Cycle analytics getters', () => {
beforeEach(() => { beforeEach(() => {
const fullPath = 'cool-beans'; const fullPath = 'cool-beans';
state = { state = {
selectedGroup: { currentGroup: {
fullPath, fullPath,
}, },
startDate, startDate,
......
...@@ -5,7 +5,7 @@ import * as actions from 'ee/analytics/cycle_analytics/store/modules/custom_stag ...@@ -5,7 +5,7 @@ import * as actions from 'ee/analytics/cycle_analytics/store/modules/custom_stag
import * as types from 'ee/analytics/cycle_analytics/store/modules/custom_stages/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/modules/custom_stages/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { selectedGroup, endpoints, rawCustomStage } from '../../../mock_data'; import { currentGroup, endpoints, rawCustomStage } from '../../../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -25,7 +25,7 @@ describe('Custom stage actions', () => { ...@@ -25,7 +25,7 @@ describe('Custom stage actions', () => {
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
state = { selectedGroup: null }; state = { currentGroup: null };
}); });
describe('createStage', () => { describe('createStage', () => {
...@@ -37,7 +37,7 @@ describe('Custom stage actions', () => { ...@@ -37,7 +37,7 @@ describe('Custom stage actions', () => {
}; };
beforeEach(() => { beforeEach(() => {
state = { ...state, selectedGroup }; state = { ...state, currentGroup };
mock.onPost(endpoints.baseStagesEndpointstageData).reply(201, customStageData); mock.onPost(endpoints.baseStagesEndpointstageData).reply(201, customStageData);
}); });
...@@ -70,7 +70,7 @@ describe('Custom stage actions', () => { ...@@ -70,7 +70,7 @@ describe('Custom stage actions', () => {
}; };
beforeEach(() => { beforeEach(() => {
state = { ...state, selectedGroup }; state = { ...state, currentGroup };
mock mock
.onPost(endpoints.baseStagesEndpointstageData) .onPost(endpoints.baseStagesEndpointstageData)
.reply(httpStatusCodes.UNPROCESSABLE_ENTITY, { .reply(httpStatusCodes.UNPROCESSABLE_ENTITY, {
......
...@@ -62,7 +62,6 @@ describe('Cycle analytics mutations', () => { ...@@ -62,7 +62,6 @@ describe('Cycle analytics mutations', () => {
it.each` it.each`
mutation | payload | expectedState mutation | payload | expectedState
${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }} ${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }}
${types.SET_SELECTED_GROUP} | ${{ fullPath: 'cool-beans' }} | ${{ selectedGroup: { fullPath: 'cool-beans' }, selectedProjects: [] }}
${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }} ${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }} ${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }} ${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }}
...@@ -176,7 +175,6 @@ describe('Cycle analytics mutations', () => { ...@@ -176,7 +175,6 @@ describe('Cycle analytics mutations', () => {
it.each` it.each`
stateKey | expectedState stateKey | expectedState
${'isLoading'} | ${true} ${'isLoading'} | ${true}
${'selectedGroup'} | ${initialData.group}
${'selectedProjects'} | ${initialData.selectedProjects} ${'selectedProjects'} | ${initialData.selectedProjects}
${'startDate'} | ${initialData.createdAfter} ${'startDate'} | ${initialData.createdAfter}
${'endDate'} | ${initialData.createdBefore} ${'endDate'} | ${initialData.createdBefore}
......
...@@ -11147,6 +11147,9 @@ msgstr "" ...@@ -11147,6 +11147,9 @@ msgstr ""
msgid "Filter by user" msgid "Filter by user"
msgstr "" msgstr ""
msgid "Filter parameters are not valid. Make sure that the end date is after the start date."
msgstr ""
msgid "Filter pipelines" msgid "Filter pipelines"
msgstr "" msgstr ""
...@@ -24192,9 +24195,6 @@ msgstr "" ...@@ -24192,9 +24195,6 @@ msgstr ""
msgid "Start and due date" msgid "Start and due date"
msgstr "" msgstr ""
msgid "Start by choosing a group to see how your team is spending time. You can then drill down to the project level."
msgstr ""
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors." msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors."
msgstr "" 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