Commit 542f6552 authored by Phil Hughes's avatar Phil Hughes

Merge branch...

Merge branch '207045-preserve-date-filters-in-value-stream-and-productivity-analytics' into 'master'

Preserve date filters in analytics

Closes #207045

See merge request gitlab-org/gitlab!27102
parents 4eba0293 6bfaf71c
......@@ -13,7 +13,8 @@ import StageDropdownFilter from './stage_dropdown_filter.vue';
import SummaryTable from './summary_table.vue';
import StageTable from './stage_table.vue';
import TasksByTypeChart from './tasks_by_type_chart.vue';
import UrlSyncMixin from '../mixins/url_sync_mixin';
import UrlSyncMixin from '../../shared/mixins/url_sync_mixin';
import { toYmd } from '../../shared/utils';
export default {
name: 'CycleAnalytics',
......@@ -125,6 +126,14 @@ export default {
selectedLabelIds,
};
},
query() {
return {
group_id: !this.hideGroupDropDown ? this.currentGroupPath : null,
'project_ids[]': this.selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
};
},
},
mounted() {
this.setFeatureFlags({
......
......@@ -8,13 +8,17 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Icon from '~/vue_shared/components/icon.vue';
import { beginOfDayTime, endOfDayTime } from '~/lib/utils/datetime_utility';
import MetricChart from './metric_chart.vue';
import Scatterplot from '../../shared/components/scatterplot.vue';
import MergeRequestTable from './mr_table.vue';
import { chartKeys } from '../constants';
import { dateFormats } from '../../shared/constants';
import urlSyncMixin from '../../shared/mixins/url_sync_mixin';
export default {
components: {
......@@ -32,7 +36,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [featureFlagsMixin()],
mixins: [featureFlagsMixin(), urlSyncMixin],
props: {
emptyStateSvgPath: {
type: String,
......@@ -42,6 +46,11 @@ export default {
type: String,
required: true,
},
hideGroupDropDown: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -49,7 +58,15 @@ export default {
};
},
computed: {
...mapState('filters', ['groupNamespace']),
...mapState('filters', [
'groupNamespace',
'projectPath',
'authorUsername',
'labelName',
'milestoneTitle',
'startDate',
'endDate',
]),
...mapState('table', ['isLoadingTable', 'mergeRequests', 'pageInfo', 'columnMetric']),
...mapGetters(['getMetricTypes']),
...mapGetters('charts', [
......@@ -86,6 +103,17 @@ export default {
showSecondaryCharts() {
return !this.chartLoading(chartKeys.main) && this.chartHasData(chartKeys.main);
},
query() {
return {
group_id: !this.hideGroupDropDown ? this.groupNamespace : null,
project_id: this.projectPath,
author_username: this.authorUsername,
'label_name[]': this.labelName,
milestone_title: this.milestoneTitle,
merged_after: `${dateFormat(this.startDate, dateFormats.isoDate)}${beginOfDayTime}`,
merged_before: `${dateFormat(this.endDate, dateFormats.isoDate)}${endOfDayTime}`,
};
},
},
mounted() {
this.setChartEnabled({
......
......@@ -172,6 +172,7 @@ export default () => {
props: {
emptyStateSvgPath,
noAccessSvgPath,
hideGroupDropDown,
},
});
},
......
import dateFormat from 'dateformat';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { beginOfDayTime, endOfDayTime } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
import { chartKeys } from '../../../constants';
import { dateFormats } from '../../../../shared/constants';
export const setInitialData = ({ commit, dispatch }, { skipFetch = false, data }) => {
commit(types.SET_INITIAL_DATA, data);
......@@ -21,8 +16,6 @@ export const setInitialData = ({ commit, dispatch }, { skipFetch = false, data }
export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
commit(types.SET_GROUP_NAMESPACE, groupNamespace);
historyPushState(setUrlParams({ group_id: groupNamespace }, window.location.href, true));
// let's reset the current selection first
// with skipReload=true we avoid data from being fetched here
dispatch('charts/resetMainChartSelection', true, { root: true });
......@@ -36,17 +29,9 @@ export const setGroupNamespace = ({ commit, dispatch }, groupNamespace) => {
});
};
export const setProjectPath = ({ commit, dispatch, state }, projectPath) => {
export const setProjectPath = ({ commit, dispatch }, projectPath) => {
commit(types.SET_PROJECT_PATH, projectPath);
historyPushState(
setUrlParams(
{ group_id: state.groupNamespace, project_id: projectPath },
window.location.href,
true,
),
);
dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
......@@ -66,8 +51,6 @@ export const setFilters = (
milestoneTitle: milestone_title,
});
historyPushState(setUrlParams({ author_username, 'label_name[]': label_name, milestone_title }));
dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
......@@ -80,11 +63,6 @@ export const setFilters = (
export const setDateRange = ({ commit, dispatch }, { startDate, endDate }) => {
commit(types.SET_DATE_RANGE, { startDate, endDate });
const mergedAfter = `${dateFormat(startDate, dateFormats.isoDate)}${beginOfDayTime}`;
const mergedBefore = `${dateFormat(endDate, dateFormats.isoDate)}${endOfDayTime}`;
historyPushState(setUrlParams({ merged_after: mergedAfter, merged_before: mergedBefore }));
dispatch('charts/resetMainChartSelection', true, { root: true });
return dispatch('charts/fetchChartData', chartKeys.main, { root: true }).then(() => {
......
import { mapState, mapGetters } from 'vuex';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { toYmd } from '../../shared/utils';
export default {
computed: {
...mapGetters(['currentGroupPath', 'selectedProjectIds']),
...mapState(['startDate', 'endDate']),
query() {
return {
group_id: this.currentGroupPath,
'project_ids[]': this.selectedProjectIds,
created_after: toYmd(this.startDate),
created_before: toYmd(this.endDate),
};
},
},
watch: {
query() {
historyPushState(setUrlParams(this.query, window.location.href, true));
......
---
title: Preserve date filters in value stream and productivity analytics
merge_request: 27102
author:
type: fixed
......@@ -22,7 +22,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import { toYmd } from 'ee/analytics/shared/utils';
import * as mockData from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import UrlSyncMixin from 'ee/analytics/cycle_analytics/mixins/url_sync_mixin';
import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
......@@ -621,7 +621,14 @@ describe('Cycle Analytics component', () => {
});
});
describe('Url Sync', () => {
describe('Url parameters', () => {
const fakeGroup = {
id: 2,
path: 'new-test',
fullPath: 'new-test-group',
name: 'New test group',
};
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
......@@ -649,14 +656,45 @@ describe('Cycle Analytics component', () => {
});
});
describe('with a group selected', () => {
const fakeGroup = {
id: 2,
path: 'new-test',
fullPath: 'new-test-group',
name: 'New test group',
};
describe('with hideGroupDropDown=true', () => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
mock = new MockAdapter(axios);
wrapper = createComponent({
shallow: false,
scatterplotEnabled: false,
tasksByTypeChartEnabled: false,
stubs: {
...defaultStubs,
},
props: {
hideGroupDropDown: true,
},
});
wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: mockData.startDate,
createdBefore: mockData.endDate,
group: fakeGroup,
});
return wrapper.vm.$nextTick();
});
it('sets the group_id url parameter', () => {
return shouldSetUrlParams({
created_after: toYmd(mockData.startDate),
created_before: toYmd(mockData.endDate),
group_id: null,
'project_ids[]': [],
});
});
});
describe('with a group selected', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('setSelectedGroup', {
...fakeGroup,
......
import Vue from 'vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import store from 'ee/analytics/cycle_analytics/store';
import UrlSyncMixin from 'ee/analytics/cycle_analytics/mixins/url_sync_mixin';
import { toYmd } from 'ee/analytics/shared/utils';
import { startDate, endDate } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = () => {
const Component = Vue.extend({
localVue,
store,
mixins: [UrlSyncMixin],
render(h) {
return h('div');
},
});
return shallowMount(Component);
};
describe('UrlSyncMixin', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
wrapper.vm.$store.dispatch('initializeCycleAnalytics', {
createdAfter: startDate,
createdBefore: endDate,
});
});
afterEach(() => {
wrapper.vm.$destroy();
});
describe('watch', () => {
describe('query', () => {
const defaultState = {
group_id: null,
'project_ids[]': [],
created_after: toYmd(startDate),
created_before: toYmd(endDate),
};
it('sets the start and end date to the default state values', () => {
expect(wrapper.vm.query).toEqual(defaultState);
});
it.each`
param | action | payload | updatedParams
${'group_id'} | ${'setSelectedGroup'} | ${{ fullPath: 'test-group', name: 'test group' }} | ${{ group_id: 'test-group' }}
${'project_ids'} | ${'setSelectedProjects'} | ${[{ id: 1 }, { id: 2 }]} | ${{ 'project_ids[]': [1, 2] }}
${'created_after'} | ${'setDateRange'} | ${{ startDate: '2020-06-18', endDate, skipFetch: true }} | ${{ created_after: toYmd('2020-06-18') }}
${'created_before'} | ${'setDateRange'} | ${{ endDate: '2020-06-18', startDate, skipFetch: true }} | ${{ created_before: toYmd('2020-06-18') }}
`(
'sets the $param parameter when $action is dispatched',
({ action, payload, updatedParams }) => {
wrapper.vm.$store.dispatch(action, payload);
expect(wrapper.vm.query).toEqual({
...defaultState,
...updatedParams,
});
},
);
});
});
});
......@@ -10,6 +10,9 @@ import { chartKeys } from 'ee/analytics/productivity_analytics/constants';
import { TEST_HOST } from 'helpers/test_constants';
import { GlEmptyState, GlLoadingIcon, GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -33,11 +36,15 @@ describe('ProductivityApp component', () => {
const mainChartData = { 1: 2, 2: 3 };
const createComponent = (scatterplotEnabled = true) => {
const createComponent = ({ props = {}, scatterplotEnabled = true } = {}) => {
wrapper = shallowMount(ProductivityApp, {
localVue,
store,
propsData,
mixins: [UrlSyncMixin],
propsData: {
...propsData,
...props,
},
methods: {
...actionSpies,
},
......@@ -347,7 +354,7 @@ describe('ProductivityApp component', () => {
describe('when the feature flag is disabled', () => {
beforeEach(() => {
createComponent(false);
createComponent({ scatterplotEnabled: false });
});
it('isScatterplotFeatureEnabled returns false', () => {
......@@ -490,4 +497,116 @@ describe('ProductivityApp component', () => {
});
});
});
describe('Url parameters', () => {
const defaultFilters = {
author_username: null,
milestone_title: null,
label_name: [],
};
const defaultResults = {
project_id: null,
group_id: null,
merged_after: '2019-09-01T00:00:00Z',
merged_before: '2019-09-02T23:59:59Z',
'label_name[]': [],
author_username: null,
milestone_title: null,
};
const shouldSetUrlParams = result => {
expect(urlUtils.setUrlParams).toHaveBeenCalledWith(result, window.location.href, true);
expect(commonUtils.historyPushState).toHaveBeenCalled();
};
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
createComponent();
wrapper.vm.$store.dispatch('filters/setInitialData', {
skipFetch: true,
data: {
mergedAfter: new Date('2019-09-01'),
mergedBefore: new Date('2019-09-02'),
},
});
});
it('sets the default url parameters', () => {
shouldSetUrlParams(defaultResults);
});
describe('with hideGroupDropDown=true', () => {
beforeEach(() => {
commonUtils.historyPushState = jest.fn();
urlUtils.setUrlParams = jest.fn();
createComponent({ props: { hideGroupDropDown: true } });
wrapper.vm.$store.dispatch('filters/setInitialData', {
skipFetch: true,
data: {
mergedAfter: new Date('2019-09-01'),
mergedBefore: new Date('2019-09-02'),
},
});
wrapper.vm.$store.dispatch('filters/setGroupNamespace', 'earth-special-forces');
});
it('does not set the group_id', () => {
shouldSetUrlParams({
...defaultResults,
});
});
});
describe('with a group selected', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('filters/setGroupNamespace', 'earth-special-forces');
});
it('sets the group_id', () => {
shouldSetUrlParams({
...defaultResults,
group_id: 'earth-special-forces',
});
});
});
describe('with a project selected', () => {
beforeEach(() => {
wrapper.vm.$store.dispatch('filters/setProjectPath', 'earth-special-forces/frieza-saga');
});
it('sets the project_id', () => {
shouldSetUrlParams({
...defaultResults,
project_id: 'earth-special-forces/frieza-saga',
});
});
});
describe.each`
paramKey | resultKey | value
${'milestone_title'} | ${'milestone_title'} | ${'final-form'}
${'author_username'} | ${'author_username'} | ${'piccolo'}
${'label_name'} | ${'label_name[]'} | ${['who-will-win']}
`('with the $paramKey filter set', ({ paramKey, resultKey, value }) => {
beforeEach(() => {
wrapper.vm.$store.dispatch('filters/setFilters', {
...defaultFilters,
[paramKey]: value,
});
});
it(`sets the '${resultKey}' url parameter`, () => {
shouldSetUrlParams({
...defaultResults,
[resultKey]: value,
});
});
});
});
});
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import * as actions from 'ee/analytics/productivity_analytics/store/modules/filters/actions';
import * as types from 'ee/analytics/productivity_analytics/store/modules/filters/mutation_types';
import testAction from 'helpers/vuex_action_helper';
......@@ -31,10 +29,6 @@ describe('Productivity analytics filter actions', () => {
};
});
afterEach(() => {
setUrlParams.mockClear();
});
describe('setInitialData', () => {
it('commits the SET_INITIAL_DATA mutation and fetches data by default', done => {
actions
......@@ -106,21 +100,6 @@ describe('Productivity analytics filter actions', () => {
.then(done)
.catch(done.fail);
});
it('calls setUrlParams with the group_id param', done => {
actions
.setGroupNamespace(store, groupNamespace)
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith(
{ group_id: groupNamespace },
window.location.href,
true,
);
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
describe('setProjectPath', () => {
......@@ -153,21 +132,6 @@ describe('Productivity analytics filter actions', () => {
.then(done)
.catch(done.fail);
});
it('calls setUrlParams with the group_id and project_id params', done => {
actions
.setProjectPath(store, projectPath)
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith(
{ group_id: groupNamespace, project_id: projectPath },
window.location.href,
true,
);
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
describe('setFilters', () => {
......@@ -200,17 +164,6 @@ describe('Productivity analytics filter actions', () => {
.then(done)
.catch(done.fail);
});
it('calls setUrlParams with the author_username', done => {
actions
.setFilters(store, { author_username: 'root' })
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith({ author_username: 'root' });
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
describe('setDateRange', () => {
......@@ -243,20 +196,5 @@ describe('Productivity analytics filter actions', () => {
.then(done)
.catch(done.fail);
});
it('calls setUrlParams with the merged_after=startDate and merged_before=endDate', done => {
actions
.setDateRange(store, { startDate, endDate })
.then(() => {
expect(setUrlParams).toHaveBeenCalledWith({
merged_after: '2019-09-01T00:00:00Z',
merged_before: '2019-09-07T23:59:59Z',
});
expect(historyPushState).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
});
import { shallowMount } from '@vue/test-utils';
import UrlSyncMixin from 'ee/analytics/shared/mixins/url_sync_mixin';
const defaultData = {
group_id: null,
project_ids: [],
};
const createComponent = () => {
return shallowMount(
{
mixins: [UrlSyncMixin],
render(h) {
return h('div');
},
},
{
computed: {
query() {
return {
group_id: this.group_id,
project_ids: this.project_ids,
};
},
},
data() {
return { ...defaultData };
},
},
);
};
describe('UrlSyncMixin', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.vm.$destroy();
});
describe('query', () => {
it('has the default state', () => {
expect(wrapper.vm.query).toEqual(defaultData);
});
describe('with parameter changes', () => {
it.each`
param | payload | updatedParams
${'group_id'} | ${'test-group'} | ${{ group_id: 'test-group' }}
${'project_ids'} | ${[1, 2]} | ${{ project_ids: [1, 2] }}
`('is updated when the $param parameter changes', ({ param, payload, updatedParams }) => {
wrapper.setData({ [param]: payload });
expect(wrapper.vm.query).toEqual({
...defaultData,
...updatedParams,
});
});
});
});
});
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