Commit a2662bf5 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '235455-remove-vsa-duration-chart-median-line' into 'master'

Remove VSA duration chart median line

Closes #235455

See merge request gitlab-org/gitlab!39665
parents 91950c18 033dff27
......@@ -350,15 +350,6 @@ administrator can open a Rails console and disable it with the following command
Feature.disable(:cycle_analytics_scatterplot_enabled)
```
### Disabling chart median line
This chart's median line is enabled by default. If you have a self-managed instance, an
administrator can open a Rails console and disable it with the following command:
```ruby
Feature.disable(:cycle_analytics_scatterplot_median_enabled)
```
## Type of work - Tasks by type chart
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32421) in GitLab 12.10.
......
......@@ -20,7 +20,7 @@ export default {
},
computed: {
...mapState('durationChart', ['isLoading']),
...mapGetters('durationChart', ['durationChartPlottableData', 'durationChartMedianData']),
...mapGetters('durationChart', ['durationChartPlottableData']),
hasData() {
return Boolean(this.durationChartPlottableData.length);
},
......@@ -53,7 +53,6 @@ export default {
:y-axis-title="s__('CycleAnalytics|Total days to completion')"
:tooltip-date-format="$options.durationChartTooltipDateFormat"
:scatter-data="durationChartPlottableData"
:median-line-data="durationChartMedianData"
/>
<div v-else ref="duration-chart-no-data" class="bs-callout bs-callout-info">
{{ __('There is no data available. Please change your selection.') }}
......
......@@ -14,7 +14,6 @@ export default () => {
const store = createStore();
const {
cycleAnalyticsScatterplotEnabled: hasDurationChart = false,
cycleAnalyticsScatterplotMedianEnabled: hasDurationChartMedian = false,
valueStreamAnalyticsPathNavigation: hasPathNavigation = false,
valueStreamAnalyticsCreateMultipleValueStreams: hasCreateMultipleValueStreams = false,
analyticsSimilaritySearch: hasAnalyticsSimilaritySearch = false,
......@@ -24,7 +23,6 @@ export default () => {
...initialData,
featureFlags: {
hasDurationChart,
hasDurationChartMedian,
hasPathNavigation,
hasCreateMultipleValueStreams,
hasAnalyticsSimilaritySearch,
......
import dateFormat from 'dateformat';
import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
import { dateFormats } from '../../../../shared/constants';
export const requestDurationData = ({ commit }) => commit(types.REQUEST_DURATION_DATA);
export const receiveDurationDataSuccess = ({ commit, rootState, dispatch }, data) => {
commit(types.RECEIVE_DURATION_DATA_SUCCESS, data);
const { featureFlags: { hasDurationChartMedian = false } = {} } = rootState;
if (hasDurationChartMedian) dispatch('fetchDurationMedianData');
};
export const receiveDurationDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration data.'));
};
export const fetchDurationData = ({ dispatch, rootGetters }) => {
export const fetchDurationData = ({ dispatch, commit, rootGetters }) => {
dispatch('requestDurationData');
const {
cycleAnalyticsRequestParams,
......@@ -44,50 +34,10 @@ export const fetchDurationData = ({ dispatch, rootGetters }) => {
}));
}),
)
.then(data => dispatch('receiveDurationDataSuccess', data))
.then(data => commit(types.RECEIVE_DURATION_DATA_SUCCESS, data))
.catch(() => dispatch('receiveDurationDataError'));
};
export const receiveDurationMedianDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS, data);
export const receiveDurationMedianDataError = ({ commit }) => {
commit(types.RECEIVE_DURATION_MEDIAN_DATA_ERROR);
createFlash(__('There was an error while fetching value stream analytics duration median data.'));
};
export const fetchDurationMedianData = ({ dispatch, rootState, rootGetters }) => {
const { startDate, endDate } = rootState;
const {
cycleAnalyticsRequestParams,
activeStages,
currentGroupPath,
currentValueStreamId,
} = rootGetters;
const offsetValue = getDayDifference(new Date(startDate), new Date(endDate));
const offsetCreatedAfter = getDateInPast(new Date(startDate), offsetValue);
const offsetCreatedBefore = getDateInPast(new Date(endDate), offsetValue);
return Promise.all(
activeStages.map(stage => {
const { slug } = stage;
return Api.cycleAnalyticsDurationChart(currentGroupPath, currentValueStreamId, slug, {
...cycleAnalyticsRequestParams,
created_after: dateFormat(offsetCreatedAfter, dateFormats.isoDate),
created_before: dateFormat(offsetCreatedBefore, dateFormats.isoDate),
}).then(({ data }) => ({
slug,
selected: true,
data,
}));
}),
)
.then(data => dispatch('receiveDurationMedianDataSuccess', data))
.catch(() => dispatch('receiveDurationMedianDataError'));
};
export const updateSelectedDurationChartStages = ({ state, commit }, stages) => {
const setSelectedPropertyOnStages = data =>
data.map(stage => {
......@@ -102,12 +52,10 @@ export const updateSelectedDurationChartStages = ({ state, commit }, stages) =>
};
});
const { durationData, durationMedianData } = state;
const { durationData } = state;
const updatedDurationStageData = setSelectedPropertyOnStages(durationData);
const updatedDurationStageMedianData = setSelectedPropertyOnStages(durationMedianData);
commit(types.UPDATE_SELECTED_DURATION_CHART_STAGES, {
updatedDurationStageData,
updatedDurationStageMedianData,
});
};
import { getDurationChartData, getDurationChartMedianData } from '../../../utils';
import { getDurationChartData } from '../../../utils';
export const durationChartPlottableData = (state, _, rootState) => {
const { startDate, endDate } = rootState;
......@@ -8,16 +8,3 @@ export const durationChartPlottableData = (state, _, rootState) => {
return plottableData.length ? plottableData : [];
};
export const durationChartMedianData = (state, _, rootState) => {
const { startDate, endDate } = rootState;
const { durationMedianData } = state;
const selectedStagesDurationMedianData = durationMedianData.filter(stage => stage.selected);
const plottableData = getDurationChartMedianData(
selectedStagesDurationMedianData,
startDate,
endDate,
);
return plottableData.length ? plottableData : [];
};
......@@ -3,6 +3,3 @@ export const UPDATE_SELECTED_DURATION_CHART_STAGES = 'UPDATE_SELECTED_DURATION_C
export const REQUEST_DURATION_DATA = 'REQUEST_DURATION_DATA';
export const RECEIVE_DURATION_DATA_SUCCESS = 'RECEIVE_DURATION_DATA_SUCCESS';
export const RECEIVE_DURATION_DATA_ERROR = 'RECEIVE_DURATION_DATA_ERROR';
export const RECEIVE_DURATION_MEDIAN_DATA_SUCCESS = 'RECEIVE_DURATION_MEDIAN_DATA_SUCCESS';
export const RECEIVE_DURATION_MEDIAN_DATA_ERROR = 'RECEIVE_DURATION_MEDIAN_DATA_ERROR';
import * as types from './mutation_types';
export default {
[types.UPDATE_SELECTED_DURATION_CHART_STAGES](
state,
{ updatedDurationStageData, updatedDurationStageMedianData },
) {
[types.UPDATE_SELECTED_DURATION_CHART_STAGES](state, { updatedDurationStageData }) {
state.durationData = updatedDurationStageData;
state.durationMedianData = updatedDurationStageMedianData;
},
[types.REQUEST_DURATION_DATA](state) {
state.isLoading = true;
......@@ -19,10 +15,4 @@ export default {
state.durationData = [];
state.isLoading = false;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS](state, data) {
state.durationMedianData = data;
},
[types.RECEIVE_DURATION_MEDIAN_DATA_ERROR](state) {
state.durationMedianData = [];
},
};
......@@ -2,5 +2,4 @@ export default () => ({
isLoading: false,
durationData: [],
durationMedianData: [],
});
......@@ -10,9 +10,6 @@ import {
dayAfter,
secondsToDays,
getDatesInRange,
getDayDifference,
getDateInPast,
getDateInFuture,
parseSeconds,
} from '~/lib/utils/datetime_utility';
import { dateFormats } from '../shared/constants';
......@@ -215,42 +212,6 @@ export const getDurationChartData = (data, startDate, endDate) => {
return eventData;
};
/**
* Takes the offset duration data for selected stages and calls getDurationChartData to compute the totals.
* The data is then transformed into a format expected by the scatterplot;
*
* [
* ['2019-09-02', 7],
* ...
* ]
*
* The transformation works by calling getDateInPast on the provided startDate and endDate in order to match
* the startDate and endDate fetched when making the API call to fetch the data.
*
* In order to map the offset data to plottable points within the chart's range, getDateInFuture is called
* on the data series with the same offest used for getDateInPast. This creates plottable data that matches up
* with the data being displayed on the chart.
*
* @param {Array} data - The computed, plottable duration chart data
* @param {Date} startDate - The globally selected cycle analytics start date
* @param {Date} endDate - The globally selected cycle analytics end date
* @returns {Array} An array with each item being another arry of two items (date, computed total)
*/
export const getDurationChartMedianData = (data, startDate, endDate) => {
const offsetValue = getDayDifference(startDate, endDate);
const offsetEndDate = getDateInPast(endDate, offsetValue);
const offsetStartDate = getDateInPast(startDate, offsetValue);
const offsetDurationData = getDurationChartData(data, offsetStartDate, offsetEndDate);
const result = offsetDurationData.map(event => [
dateFormat(getDateInFuture(new Date(event[0]), offsetValue), dateFormats.isoDate),
event[1],
]);
return result;
};
export const orderByDate = (a, b, dateFmt = datetime => new Date(datetime).getTime()) =>
dateFmt(a) - dateFmt(b);
......
......@@ -13,7 +13,6 @@ class Analytics::CycleAnalyticsController < Analytics::ApplicationController
before_action do
push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true)
push_frontend_feature_flag(:cycle_analytics_scatterplot_median_enabled, default_enabled: true)
push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group)
push_frontend_feature_flag(:value_stream_analytics_create_multiple_value_streams, default_enabled: true)
push_frontend_feature_flag(:analytics_similarity_search, @group, default_enabled: true)
......
---
title: Remove VSA duration median line
merge_request: 39665
author:
type: removed
......@@ -19,7 +19,6 @@ RSpec.describe 'Group value stream analytics' do
expect(page).to have_pushed_frontend_feature_flags(
cycleAnalyticsScatterplotEnabled: true,
cycleAnalyticsScatterplotMedianEnabled: true,
valueStreamAnalyticsPathNavigation: true,
analyticsSimilaritySearch: true
)
......
......@@ -6,6 +6,6 @@ exports[`DurationChart renders the duration chart 1`] = `
<h4 class=\\"mt-0\\">Days to completion</h4>
<stagedropdownfilter-stub stages=\\"[object Object],[object Object],[object Object]\\" label=\\"stage dropdown\\" class=\\"ml-auto\\"></stagedropdownfilter-stub>
</div>
<scatterplot-stub xaxistitle=\\"Date\\" yaxistitle=\\"Total days to completion\\" scatterdata=\\"2019-01-01,29,2019-01-01,2019-01-02,100,2019-01-02\\" medianlinedata=\\"2018-12-31,29,2019-01-01,100\\" tooltipdateformat=\\"mmm d, yyyy\\"></scatterplot-stub>
<scatterplot-stub xaxistitle=\\"Date\\" yaxistitle=\\"Total days to completion\\" scatterdata=\\"2019-01-01,29,2019-01-01,2019-01-02,100,2019-01-02\\" medianlinedata=\\"\\" tooltipdateformat=\\"mmm d, yyyy\\"></scatterplot-stub>
</div>"
`;
......@@ -50,7 +50,6 @@ const defaultStubs = {
const defaultFeatureFlags = {
hasDurationChart: true,
hasDurationChartMedian: true,
hasPathNavigation: false,
hasCreateMultipleValueStreams: false,
};
......
......@@ -5,11 +5,7 @@ import durationChartStore from 'ee/analytics/cycle_analytics/store/modules/durat
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
import {
allowedStages as stages,
durationChartPlottableData as durationData,
durationChartPlottableMedianData as durationMedianData,
} from '../mock_data';
import { allowedStages as stages, durationChartPlottableData as durationData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -26,7 +22,6 @@ const fakeStore = ({ initialGetters, initialState }) =>
...durationChartStore,
getters: {
durationChartPlottableData: () => durationData,
durationChartMedianData: () => durationMedianData,
...initialGetters,
},
state: {
......@@ -130,7 +125,6 @@ describe('DurationChart', () => {
wrapper = createComponent({
initialGetters: {
durationChartPlottableData: () => [],
durationChartMedianData: () => [],
},
});
});
......
......@@ -252,21 +252,6 @@ export const rawDurationMedianData = [
},
];
export const transformedDurationMedianData = [
{
slug: 1,
selected: true,
data: rawDurationMedianData,
},
{
slug: 2,
selected: true,
data: rawDurationMedianData,
},
];
export const durationChartPlottableMedianData = [['2018-12-31', 29], ['2019-01-01', 100]];
export const selectedProjects = [
{
id: 1,
......
......@@ -58,7 +58,6 @@ describe('Cycle analytics actions', () => {
stages: [],
featureFlags: {
hasDurationChart: true,
hasDurationChartMedian: true,
},
activeStages,
selectedValueStream,
......
......@@ -11,9 +11,7 @@ import {
startDate,
endDate,
rawDurationData,
rawDurationMedianData,
transformedDurationData,
transformedDurationMedianData,
endpoints,
valueStreams,
} from '../../../mock_data';
......@@ -33,7 +31,6 @@ const rootState = {
selectedValueStream,
featureFlags: {
hasDurationChart: true,
hasDurationChartMedian: true,
},
};
......@@ -66,14 +63,13 @@ describe('DurationChart actions', () => {
actions.fetchDurationData,
null,
state,
[],
[
{ type: 'requestDurationData' },
{
type: 'receiveDurationDataSuccess',
type: types.RECEIVE_DURATION_DATA_SUCCESS,
payload: transformedDurationData,
},
],
[{ type: 'requestDurationData' }],
);
});
......@@ -120,53 +116,6 @@ describe('DurationChart actions', () => {
});
});
describe('receiveDurationDataSuccess', () => {
describe('with hasDurationChartMedian feature flag enabled', () => {
it('commits the transformed duration data and dispatches fetchDurationMedianData', () => {
testAction(
actions.receiveDurationDataSuccess,
transformedDurationData,
rootState,
[
{
type: types.RECEIVE_DURATION_DATA_SUCCESS,
payload: transformedDurationData,
},
],
[
{
type: 'fetchDurationMedianData',
},
],
);
});
});
describe('with hasDurationChartMedian feature flag disabled', () => {
const disabledState = {
...rootState,
featureFlags: {
hasDurationChartMedian: false,
},
};
it('commits the transformed duration data', () => {
testAction(
actions.receiveDurationDataSuccess,
transformedDurationData,
disabledState,
[
{
type: types.RECEIVE_DURATION_DATA_SUCCESS,
payload: transformedDurationData,
},
],
[],
);
});
});
});
describe('receiveDurationDataError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
......@@ -202,7 +151,6 @@ describe('DurationChart actions', () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
durationMedianData: transformedDurationMedianData,
};
testAction(
......@@ -214,7 +162,6 @@ describe('DurationChart actions', () => {
type: types.UPDATE_SELECTED_DURATION_CHART_STAGES,
payload: {
updatedDurationStageData: transformedDurationData,
updatedDurationStageMedianData: transformedDurationMedianData,
},
},
],
......@@ -226,7 +173,6 @@ describe('DurationChart actions', () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
durationMedianData: transformedDurationMedianData,
};
testAction(
......@@ -244,13 +190,6 @@ describe('DurationChart actions', () => {
selected: false,
},
],
updatedDurationStageMedianData: [
transformedDurationMedianData[0],
{
...transformedDurationMedianData[1],
selected: false,
},
],
},
},
],
......@@ -262,7 +201,6 @@ describe('DurationChart actions', () => {
const stateWithDurationData = {
...state,
durationData: transformedDurationData,
durationMedianData: transformedDurationMedianData,
};
testAction(
......@@ -283,16 +221,6 @@ describe('DurationChart actions', () => {
selected: false,
},
],
updatedDurationStageMedianData: [
{
...transformedDurationMedianData[0],
selected: false,
},
{
...transformedDurationMedianData[1],
selected: false,
},
],
},
},
],
......@@ -300,95 +228,4 @@ describe('DurationChart actions', () => {
);
});
});
describe('fetchDurationMedianData', () => {
beforeEach(() => {
mock.onGet(endpoints.durationData).reply(200, [...rawDurationMedianData]);
});
it('dispatches the receiveDurationMedianDataSuccess action on success', () => {
return testAction(
actions.fetchDurationMedianData,
null,
state,
[],
[
{
type: 'receiveDurationMedianDataSuccess',
payload: transformedDurationMedianData,
},
],
);
});
describe('receiveDurationMedianDataError', () => {
beforeEach(() => {
mock.onGet(endpoints.durationData).reply(404);
});
it('dispatches the receiveDurationMedianDataError action when there is an error', () => {
const dispatch = jest.fn();
return actions
.fetchDurationMedianData({
dispatch,
rootState,
rootGetters: { ...rootGetters, activeStages },
})
.then(() => {
const requestedUrls = mock.history.get.map(({ url }) => url);
expect(requestedUrls).not.toContain(
`/groups/foo/-/analytics/value_stream_analytics/stages/${hiddenStage.id}/duration_chart`,
);
expect(dispatch).toHaveBeenCalledWith('receiveDurationMedianDataError');
});
});
});
});
describe('receiveDurationMedianDataSuccess', () => {
it('commits the transformed duration median data', () => {
return testAction(
actions.receiveDurationMedianDataSuccess,
transformedDurationMedianData,
rootState,
[
{
type: types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS,
payload: transformedDurationMedianData,
},
],
[],
);
});
});
describe('receiveDurationMedianDataError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it("commits the 'RECEIVE_DURATION_MEDIAN_DATA_ERROR' mutation", () => {
return testAction(
actions.receiveDurationMedianDataError,
{},
rootState,
[
{
type: types.RECEIVE_DURATION_MEDIAN_DATA_ERROR,
},
],
[],
);
});
it('will flash an error', () => {
actions.receiveDurationMedianDataError({
commit: () => {},
});
shouldFlashAMessage(
'There was an error while fetching value stream analytics duration median data.',
);
});
});
});
......@@ -3,9 +3,7 @@ import {
startDate,
endDate,
transformedDurationData,
transformedDurationMedianData,
durationChartPlottableData,
durationChartPlottableMedianData,
} from '../../../mock_data';
const rootState = {
......@@ -35,28 +33,4 @@ describe('DurationChart getters', () => {
);
});
});
describe('durationChartPlottableMedianData', () => {
it('returns plottable median data for selected stages', () => {
const stateWithDurationMedianData = {
durationMedianData: transformedDurationMedianData,
};
expect(
getters.durationChartMedianData(stateWithDurationMedianData, getters, rootState),
).toEqual(durationChartPlottableMedianData);
});
it('returns an empty array if there is no plottable median data for the selected stages', () => {
const stateWithDurationMedianData = {
startDate,
endDate,
durationMedianData: [],
};
expect(
getters.durationChartMedianData(stateWithDurationMedianData, getters, rootState),
).toEqual([]);
});
});
});
import mutations from 'ee/analytics/cycle_analytics/store/modules/duration_chart/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/modules/duration_chart/mutation_types';
import { transformedDurationData, transformedDurationMedianData } from '../../../mock_data';
import { transformedDurationData } from '../../../mock_data';
let state = null;
......@@ -25,8 +25,8 @@ describe('DurationChart mutations', () => {
});
it.each`
mutation | payload | expectedState
${types.UPDATE_SELECTED_DURATION_CHART_STAGES} | ${{ updatedDurationStageData: transformedDurationData, updatedDurationStageMedianData: transformedDurationMedianData }} | ${{ durationData: transformedDurationData, durationMedianData: transformedDurationMedianData }}
mutation | payload | expectedState
${types.UPDATE_SELECTED_DURATION_CHART_STAGES} | ${{ updatedDurationStageData: transformedDurationData }} | ${{ durationData: transformedDurationData }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......@@ -52,31 +52,4 @@ describe('DurationChart mutations', () => {
expect(stateWithData.durationData).toBe(transformedDurationData);
});
});
describe(`${types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS}`, () => {
it('sets the data correctly', () => {
const stateWithData = {
durationMedianData: [['something', 'random']],
};
mutations[types.RECEIVE_DURATION_MEDIAN_DATA_SUCCESS](
stateWithData,
transformedDurationMedianData,
);
expect(stateWithData.durationMedianData).toBe(transformedDurationMedianData);
});
});
describe(`${types.RECEIVE_DURATION_MEDIAN_DATA_ERROR}`, () => {
it('sets durationMedianData to an empty array', () => {
const stateWithData = {
durationMedianData: [['something', 'random']],
};
mutations[types.RECEIVE_DURATION_MEDIAN_DATA_ERROR](stateWithData);
expect(stateWithData.durationMedianData).toStrictEqual([]);
});
});
});
......@@ -8,7 +8,6 @@ import {
getLabelEventsIdentifiers,
flattenDurationChartData,
getDurationChartData,
getDurationChartMedianData,
transformRawStages,
isPersistedStage,
getTasksByTypeData,
......@@ -28,10 +27,8 @@ import {
labelStartEvent,
customStageStartEvents as startEvents,
transformedDurationData,
transformedDurationMedianData,
flattenedDurationData,
durationChartPlottableData,
durationChartPlottableMedianData,
startDate,
endDate,
issueStage,
......@@ -151,18 +148,6 @@ describe('Cycle analytics utils', () => {
});
});
describe('getDurationChartMedianData', () => {
it('computes the plottable data as expected', () => {
const plottableData = getDurationChartMedianData(
transformedDurationMedianData,
startDate,
endDate,
);
expect(plottableData).toStrictEqual(durationChartPlottableMedianData);
});
});
describe('transformRawStages', () => {
it('retains all the stage properties', () => {
const transformed = transformRawStages([issueStage, rawCustomStage]);
......
......@@ -24992,9 +24992,6 @@ msgstr ""
msgid "There was an error while fetching value stream analytics duration data."
msgstr ""
msgid "There was an error while fetching value stream analytics duration median data."
msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
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