Commit 4ab1ad04 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '346554-vsa-add-total-time-chart-each-stage' into 'master'

Add the total time chart to each VSA stage

See merge request gitlab-org/gitlab!80255
parents c6c31507 9c048ea7
...@@ -193,18 +193,18 @@ export default { ...@@ -193,18 +193,18 @@ export default {
" "
/> />
<template v-else> <template v-else>
<div class="gl-mt-2"> <div :class="[isOverviewStageSelected ? 'gl-mt-2' : 'gl-mt-6']">
<template v-if="isOverviewStageSelected"> <value-stream-metrics
<value-stream-metrics v-if="isOverviewStageSelected"
:request-path="currentGroupPath" :request-path="currentGroupPath"
:request-params="cycleAnalyticsRequestParams" :request-params="cycleAnalyticsRequestParams"
:requests="$options.METRICS_REQUESTS" :requests="$options.METRICS_REQUESTS"
/> />
<duration-chart class="gl-mt-3" :stages="activeStages" /> <duration-chart class="gl-mt-3" :stages="activeStages" :selected-stage="selectedStage" />
<type-of-work-charts /> <type-of-work-charts v-if="isOverviewStageSelected" />
</template>
<stage-table <stage-table
v-else v-if="!isOverviewStageSelected"
class="gl-mt-5"
:is-loading="isLoading || isLoadingStage" :is-loading="isLoading || isLoadingStage"
:stage-events="selectedStageEvents" :stage-events="selectedStageEvents"
:selected-stage="selectedStage" :selected-stage="selectedStage"
......
<script> <script>
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { dataVizBlue500 } from '@gitlab/ui/scss_to_js/scss_variables';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { __ } from '~/locale'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import Scatterplot from '../../shared/components/scatterplot.vue'; import Scatterplot from '../../shared/components/scatterplot.vue';
import {
DURATION_STAGE_TIME_DESCRIPTION,
DURATION_STAGE_TIME_NO_DATA,
DURATION_STAGE_TIME_LABEL,
DURATION_TOTAL_TIME_DESCRIPTION,
DURATION_TOTAL_TIME_NO_DATA,
DURATION_TOTAL_TIME_LABEL,
} from '../constants';
import StageDropdownFilter from './stage_dropdown_filter.vue'; import StageDropdownFilter from './stage_dropdown_filter.vue';
export default { export default {
name: 'DurationChart', name: 'DurationChart',
components: { components: {
GlAlert, GlAlert,
GlIcon,
Scatterplot, Scatterplot,
StageDropdownFilter, StageDropdownFilter,
ChartSkeletonLoader, ChartSkeletonLoader,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
stages: { stages: {
type: Array, type: Array,
...@@ -22,15 +36,32 @@ export default { ...@@ -22,15 +36,32 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['selectedStage']),
...mapState('durationChart', ['isLoading', 'errorMessage']), ...mapState('durationChart', ['isLoading', 'errorMessage']),
...mapGetters(['isOverviewStageSelected']),
...mapGetters('durationChart', ['durationChartPlottableData']), ...mapGetters('durationChart', ['durationChartPlottableData']),
hasData() { hasData() {
return Boolean(!this.isLoading && this.durationChartPlottableData.length); return Boolean(!this.isLoading && this.durationChartPlottableData.length);
}, },
error() { error() {
return this.errorMessage if (this.errorMessage) {
? this.errorMessage return this.errorMessage;
: __('There is no data available. Please change your selection.'); }
return this.isOverviewStageSelected
? DURATION_TOTAL_TIME_NO_DATA
: DURATION_STAGE_TIME_NO_DATA;
},
title() {
return this.isOverviewStageSelected
? DURATION_TOTAL_TIME_LABEL
: sprintf(DURATION_STAGE_TIME_LABEL, {
title: capitalizeFirstCharacter(this.selectedStage.title),
});
},
tooltipText() {
return this.isOverviewStageSelected
? DURATION_TOTAL_TIME_DESCRIPTION
: DURATION_STAGE_TIME_DESCRIPTION;
}, },
}, },
methods: { methods: {
...@@ -40,22 +71,22 @@ export default { ...@@ -40,22 +71,22 @@ export default {
}, },
}, },
durationChartTooltipDateFormat: dateFormats.defaultDate, durationChartTooltipDateFormat: dateFormats.defaultDate,
medianAdditionalOptions: {
lineStyle: {
color: dataVizBlue500,
},
},
}; };
</script> </script>
<template> <template>
<chart-skeleton-loader v-if="isLoading" size="md" class="gl-my-4 gl-py-4" /> <chart-skeleton-loader v-if="isLoading" size="md" class="gl-my-4 gl-py-4" />
<div v-else class="gl-display-flex gl-flex-direction-column" data-testid="vsa-duration-chart"> <div v-else class="gl-display-flex gl-flex-direction-column" data-testid="vsa-duration-chart">
<h4 class="gl-mt-0">{{ s__('CycleAnalytics|Days to completion') }}</h4> <h4 class="gl-mt-0">
<p> {{ title }}&nbsp;<gl-icon v-gl-tooltip.hover name="information-o" :title="tooltipText" />
{{ </h4>
s__(
'CycleAnalytics|The average time spent in the selected stage for the items that were completed on each date. Data limited to the last 500 items.',
)
}}
</p>
<stage-dropdown-filter <stage-dropdown-filter
v-if="stages.length" v-if="isOverviewStageSelected && stages.length"
class="gl-ml-auto" class="gl-ml-auto"
:stages="stages" :stages="stages"
@selected="onDurationStageSelect" @selected="onDurationStageSelect"
...@@ -63,9 +94,11 @@ export default { ...@@ -63,9 +94,11 @@ export default {
<scatterplot <scatterplot
v-if="hasData" v-if="hasData"
:x-axis-title="s__('CycleAnalytics|Date')" :x-axis-title="s__('CycleAnalytics|Date')"
:y-axis-title="s__('CycleAnalytics|Average days to completion')" :y-axis-title="s__('CycleAnalytics|Average time to completion')"
:tooltip-date-format="$options.durationChartTooltipDateFormat" :tooltip-date-format="$options.durationChartTooltipDateFormat"
:scatter-data="durationChartPlottableData" :scatter-data="durationChartPlottableData"
:median-line-data="durationChartPlottableData"
:median-line-options="$options.medianAdditionalOptions"
/> />
<gl-alert v-else variant="info" :dismissible="false" class="gl-mt-3"> <gl-alert v-else variant="info" :dismissible="false" class="gl-mt-3">
{{ error }} {{ error }}
......
import { getGroupValueStreamMetrics } from 'ee/api/analytics_api'; import { getGroupValueStreamMetrics } from 'ee/api/analytics_api';
import { METRIC_TYPE_SUMMARY, METRIC_TYPE_TIME_SUMMARY } from '~/api/analytics_api'; import { METRIC_TYPE_SUMMARY, METRIC_TYPE_TIME_SUMMARY } from '~/api/analytics_api';
import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
export const EVENTS_LIST_ITEM_LIMIT = 50; export const EVENTS_LIST_ITEM_LIMIT = 50;
...@@ -41,3 +41,18 @@ export const METRICS_REQUESTS = [ ...@@ -41,3 +41,18 @@ export const METRICS_REQUESTS = [
name: __('recent activity'), name: __('recent activity'),
}, },
]; ];
export const DURATION_TOTAL_TIME_LABEL = s__('CycleAnalytics|Total time');
export const DURATION_TOTAL_TIME_NO_DATA = s__(
"CycleAnalytics|There is no data for 'Total time' available. Adjust the current filters.",
);
export const DURATION_TOTAL_TIME_DESCRIPTION = s__(
'CycleAnalytics|The total time items spent across each value stream stage. Data limited to items completed within this date range.',
);
export const DURATION_STAGE_TIME_LABEL = s__('CycleAnalytics|Stage time: %{title}');
export const DURATION_STAGE_TIME_NO_DATA = s__(
"CycleAnalytics|There is no data for 'Stage time' available. Adjust the current filters.",
);
export const DURATION_STAGE_TIME_DESCRIPTION = s__(
'CycleAnalytics|The average time items spent in this stage. Data limited to items completed within this date range.',
);
import { getDurationChartData } from '../../../utils'; import { getDurationChartData } from '../../../utils';
export const durationChartPlottableData = (state, _, rootState) => { export const durationChartPlottableData = (state, _, rootState, rootGetters) => {
const { createdAfter, createdBefore } = rootState; const { createdAfter, createdBefore, selectedStage } = rootState;
const { durationData } = state; const { durationData } = state;
const selectedStagesDurationData = durationData.filter((stage) => stage.selected); const { isOverviewStageSelected } = rootGetters;
const selectedStagesDurationData = isOverviewStageSelected
? durationData.filter((stage) => stage.selected)
: durationData.filter((stage) => stage.id === selectedStage.id);
const plottableData = getDurationChartData( const plottableData = getDurationChartData(
selectedStagesDurationData, selectedStagesDurationData,
createdAfter, createdAfter,
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
medianLineOptions: {
type: Object,
required: false,
default: () => ({}),
},
tooltipDateFormat: { tooltipDateFormat: {
type: String, type: String,
required: false, required: false,
...@@ -71,6 +76,7 @@ export default { ...@@ -71,6 +76,7 @@ export default {
result.push({ result.push({
data: this.medianLineData, data: this.medianLineData,
...scatterChartLineProps.default, ...scatterChartLineProps.default,
...this.medianLineOptions,
}); });
} }
......
...@@ -305,6 +305,8 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -305,6 +305,8 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
stage_name = page.find("#{path_nav_selector} .gl-path-active-item-indigo").text stage_name = page.find("#{path_nav_selector} .gl-path-active-item-indigo").text
expect(stage_name).to include(stage[:title]) expect(stage_name).to include(stage[:title])
expect(stage_name).to include(stage[:time]) expect(stage_name).to include(stage[:time])
expect(page).to have_selector('[data-testid="vsa-duration-chart"]')
end end
end end
...@@ -318,7 +320,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -318,7 +320,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
it 'will have data available' do it 'will have data available' do
duration_chart_content = page.find('[data-testid="vsa-duration-chart"]') duration_chart_content = page.find('[data-testid="vsa-duration-chart"]')
expect(duration_chart_content).not_to have_text(_("There is no data available. Please change your selection.")) expect(duration_chart_content).not_to have_text(_("There is no data available. Please change your selection."))
expect(duration_chart_content).to have_text(s_('CycleAnalytics|Average days to completion')) expect(duration_chart_content).to have_text(s_('CycleAnalytics|Average time to completion'))
tasks_by_type_chart_content = page.find('.js-tasks-by-type-chart') tasks_by_type_chart_content = page.find('.js-tasks-by-type-chart')
expect(tasks_by_type_chart_content).not_to have_text(_("There is no data available. Please change your selection.")) expect(tasks_by_type_chart_content).not_to have_text(_("There is no data available. Please change your selection."))
...@@ -333,8 +335,8 @@ RSpec.describe 'Group value stream analytics filters and data', :js do ...@@ -333,8 +335,8 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
it 'will filter the data' do it 'will filter the data' do
duration_chart_content = page.find('[data-testid="vsa-duration-chart"]') duration_chart_content = page.find('[data-testid="vsa-duration-chart"]')
expect(duration_chart_content).not_to have_text(s_('CycleAnalytics|Average days to completion')) expect(duration_chart_content).not_to have_text(s_('CycleAnalytics|Average time to completion'))
expect(duration_chart_content).to have_text(_("There is no data available. Please change your selection.")) expect(duration_chart_content).to have_text(s_("CycleAnalytics|There is no data for 'Total time' available. Adjust the current filters."))
tasks_by_type_chart_content = page.find('.js-tasks-by-type-chart') tasks_by_type_chart_content = page.find('.js-tasks-by-type-chart')
expect(tasks_by_type_chart_content).to have_text(_("There is no data available. Please change your selection.")) expect(tasks_by_type_chart_content).to have_text(_("There is no data available. Please change your selection."))
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DurationChart renders the duration chart 1`] = ` exports[`DurationChart with the overiew stage selected renders the duration chart 1`] = `
<div <div
class="gl-display-flex gl-flex-direction-column" class="gl-display-flex gl-flex-direction-column"
data-testid="vsa-duration-chart" data-testid="vsa-duration-chart"
...@@ -8,14 +8,14 @@ exports[`DurationChart renders the duration chart 1`] = ` ...@@ -8,14 +8,14 @@ exports[`DurationChart renders the duration chart 1`] = `
<h4 <h4
class="gl-mt-0" class="gl-mt-0"
> >
Days to completion
</h4>
<p>
The average time spent in the selected stage for the items that were completed on each date. Data limited to the last 500 items. Total time 
<gl-icon-stub
</p> name="information-o"
size="16"
title="The total time items spent across each value stream stage. Data limited to items completed within this date range."
/>
</h4>
<stagedropdownfilter-stub <stagedropdownfilter-stub
class="gl-ml-auto" class="gl-ml-auto"
...@@ -24,11 +24,12 @@ exports[`DurationChart renders the duration chart 1`] = ` ...@@ -24,11 +24,12 @@ exports[`DurationChart renders the duration chart 1`] = `
/> />
<scatterplot-stub <scatterplot-stub
medianlinedata="" medianlinedata="2019-01-01,17,2019-01-01,2019-01-02,40,2019-01-02"
scatterdata="2019-01-01,14,2019-01-01,2019-01-02,50,2019-01-02" medianlineoptions="[object Object]"
scatterdata="2019-01-01,17,2019-01-01,2019-01-02,40,2019-01-02"
tooltipdateformat="mmm d, yyyy" tooltipdateformat="mmm d, yyyy"
xaxistitle="Date" xaxistitle="Date"
yaxistitle="Average days to completion" yaxistitle="Average time to completion"
/> />
</div> </div>
`; `;
...@@ -326,6 +326,10 @@ describe('EE Value Stream Analytics component', () => { ...@@ -326,6 +326,10 @@ describe('EE Value Stream Analytics component', () => {
it('displays the path navigation', () => { it('displays the path navigation', () => {
displaysPathNavigation(true); displaysPathNavigation(true);
}); });
it('displays the duration chart', () => {
displaysDurationChart(true);
});
}); });
}); });
......
import { GlDropdownItem } from '@gitlab/ui'; import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import {
DURATION_STAGE_TIME_DESCRIPTION,
DURATION_TOTAL_TIME_DESCRIPTION,
DURATION_STAGE_TIME_NO_DATA,
DURATION_TOTAL_TIME_NO_DATA,
} from 'ee/analytics/cycle_analytics/constants';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.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 StageDropdownFilter from 'ee/analytics/cycle_analytics/components/stage_dropdown_filter.vue';
import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue'; import Scatterplot from 'ee/analytics/shared/components/scatterplot.vue';
...@@ -15,8 +21,15 @@ const actionSpies = { ...@@ -15,8 +21,15 @@ const actionSpies = {
updateSelectedDurationChartStages: jest.fn(), updateSelectedDurationChartStages: jest.fn(),
}; };
const fakeStore = ({ initialGetters, initialState }) => const fakeStore = ({ initialGetters, initialState, rootGetters, rootState }) =>
new Vuex.Store({ new Vuex.Store({
state: {
...rootState,
},
getters: {
isOverviewStageSelected: () => true,
...rootGetters,
},
modules: { modules: {
durationChart: { durationChart: {
namespaced: true, namespaced: true,
...@@ -38,10 +51,12 @@ function createComponent({ ...@@ -38,10 +51,12 @@ function createComponent({
stubs = {}, stubs = {},
initialState = {}, initialState = {},
initialGetters = {}, initialGetters = {},
rootGetters = {},
rootState = {},
props = {}, props = {},
} = {}) { } = {}) {
return mountFn(DurationChart, { return mountFn(DurationChart, {
store: fakeStore({ initialState, initialGetters }), store: fakeStore({ initialState, initialGetters, rootGetters, rootState }),
propsData: { propsData: {
stages, stages,
...props, ...props,
...@@ -59,6 +74,7 @@ describe('DurationChart', () => { ...@@ -59,6 +74,7 @@ describe('DurationChart', () => {
let wrapper; let wrapper;
const findContainer = (_wrapper) => _wrapper.find('[data-testid="vsa-duration-chart"]'); const findContainer = (_wrapper) => _wrapper.find('[data-testid="vsa-duration-chart"]');
const findChartDescription = (_wrapper) => _wrapper.findComponent(GlIcon);
const findScatterPlot = (_wrapper) => _wrapper.findComponent(Scatterplot); const findScatterPlot = (_wrapper) => _wrapper.findComponent(Scatterplot);
const findStageDropdown = (_wrapper) => _wrapper.findComponent(StageDropdownFilter); const findStageDropdown = (_wrapper) => _wrapper.findComponent(StageDropdownFilter);
const findLoader = (_wrapper) => _wrapper.findComponent(ChartSkeletonLoader); const findLoader = (_wrapper) => _wrapper.findComponent(ChartSkeletonLoader);
...@@ -67,31 +83,52 @@ describe('DurationChart', () => { ...@@ -67,31 +83,52 @@ describe('DurationChart', () => {
findStageDropdown(_wrapper).findAllComponents(GlDropdownItem).at(index).vm.$emit('click'); findStageDropdown(_wrapper).findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
}; };
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
it('renders the duration chart', () => { describe('with the overiew stage selected', () => {
expect(wrapper.element).toMatchSnapshot(); beforeEach(() => {
}); wrapper = createComponent({});
});
it('renders the scatter plot', () => { it('renders the duration chart', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true); expect(wrapper.element).toMatchSnapshot();
}); });
it('renders the scatter plot', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true);
});
it('renders the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(true);
});
it('renders the stage dropdown', () => { it('renders the chart description', () => {
expect(findStageDropdown(wrapper).exists()).toBe(true); expect(findChartDescription(wrapper).attributes('title')).toBe(
DURATION_TOTAL_TIME_DESCRIPTION,
);
});
describe('with no chart data', () => {
beforeEach(() => {
wrapper = createComponent({
initialGetters: {
durationChartPlottableData: () => [],
},
});
});
it('renders the no data available message', () => {
expect(findContainer(wrapper).text()).toContain(DURATION_TOTAL_TIME_NO_DATA);
});
});
}); });
describe('when a stage is selected', () => { describe('when a stage is selected', () => {
const selectedIndex = 1; const selectedIndex = 1;
const selectedStages = stages.filter((_, index) => index !== selectedIndex); const selectedStages = stages.filter((_, index) => index !== selectedIndex);
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ stubs: { StageDropdownFilter } }); wrapper = createComponent({ stubs: { StageDropdownFilter } });
selectStage(wrapper, selectedIndex); selectStage(wrapper, selectedIndex);
...@@ -105,33 +142,78 @@ describe('DurationChart', () => { ...@@ -105,33 +142,78 @@ describe('DurationChart', () => {
}); });
}); });
describe('with no stages', () => { describe('with a value stream stage selected', () => {
const [selectedStage] = stages;
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
mountFn: mount, rootState: {
props: { stages: [] }, selectedStage,
stubs: { StageDropdownFilter: false }, },
rootGetters: {
isOverviewStageSelected: () => false,
},
}); });
}); });
it('renders the scatter plot', () => {
expect(findScatterPlot(wrapper).exists()).toBe(true);
});
it('does not render the stage dropdown', () => { it('does not render the stage dropdown', () => {
expect(findStageDropdown(wrapper).exists()).toBe(false); expect(findStageDropdown(wrapper).exists()).toBe(false);
}); });
it('renders the stage title', () => {
expect(wrapper.text()).toContain(`Stage time: ${selectedStage.title}`);
});
it('sets the scatter plot data', () => {
expect(findScatterPlot(wrapper).props('scatterData')).toBe(durationData);
});
it('sets the median line data', () => {
expect(findScatterPlot(wrapper).props('medianLineData')).toBe(durationData);
});
it('renders the chart description', () => {
expect(findChartDescription(wrapper).attributes('title')).toBe(
DURATION_STAGE_TIME_DESCRIPTION,
);
});
describe('with no chart data', () => {
beforeEach(() => {
wrapper = createComponent({
initialGetters: {
durationChartPlottableData: () => [],
},
rootState: {
selectedStage,
},
rootGetters: {
isOverviewStageSelected: () => false,
},
});
});
it('renders the no data available message', () => {
expect(findContainer(wrapper).text()).toContain(DURATION_STAGE_TIME_NO_DATA);
});
});
}); });
describe('with no chart data', () => { describe('with no stages', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialGetters: { mountFn: mount,
durationChartPlottableData: () => [], props: { stages: [] },
}, stubs: { StageDropdownFilter: false },
}); });
}); });
it('renders the no data available message', () => { it('does not render the stage dropdown', () => {
expect(findContainer(wrapper).text()).toContain( expect(findStageDropdown(wrapper).exists()).toBe(false);
'There is no data available. Please change your selection.',
);
}); });
}); });
......
...@@ -249,46 +249,63 @@ export const taskByTypeFilters = { ...@@ -249,46 +249,63 @@ export const taskByTypeFilters = {
selectedLabelIds: [1, 2, 3], selectedLabelIds: [1, 2, 3],
}; };
export const rawDurationData = [ export const transformedDurationData = [
{ {
average_duration_in_seconds: 1234000, id: issueStage.id,
date: '2019-01-01T00:00:00.000Z', selected: true,
data: [
{
average_duration_in_seconds: 1134000, // ~13 days
date: '2019-01-01T00:00:00.000Z',
},
{
average_duration_in_seconds: 2321000, // ~27 days
date: '2019-01-02T00:00:00.000Z',
},
],
}, },
{ {
average_duration_in_seconds: 4321000, id: planStage.id,
date: '2019-01-02T00:00:00.000Z', selected: true,
data: [
{
average_duration_in_seconds: 2142000, // ~25 days
date: '2019-01-01T00:00:00.000Z',
},
{
average_duration_in_seconds: 3635000, // ~42 days
date: '2019-01-02T00:00:00.000Z',
},
],
},
{
id: codeStage.id,
selected: true,
data: [
{
average_duration_in_seconds: 1234000, // ~14 days
date: '2019-01-01T00:00:00.000Z',
},
{
average_duration_in_seconds: 4321000, // ~50 days
date: '2019-01-02T00:00:00.000Z',
},
],
}, },
]; ];
export const transformedDurationData = allowedStages.map(({ id }) => ({
id,
selected: true,
data: rawDurationData,
}));
export const flattenedDurationData = [ export const flattenedDurationData = [
{ average_duration_in_seconds: 1234000, date: '2019-01-01' }, { average_duration_in_seconds: 1134000, date: '2019-01-01' },
{ average_duration_in_seconds: 4321000, date: '2019-01-02' }, { average_duration_in_seconds: 2321000, date: '2019-01-02' },
{ average_duration_in_seconds: 1234000, date: '2019-01-01' }, { average_duration_in_seconds: 2142000, date: '2019-01-01' },
{ average_duration_in_seconds: 4321000, date: '2019-01-02' }, { average_duration_in_seconds: 3635000, date: '2019-01-02' },
{ average_duration_in_seconds: 1234000, date: '2019-01-01' }, { average_duration_in_seconds: 1234000, date: '2019-01-01' },
{ average_duration_in_seconds: 4321000, date: '2019-01-02' }, { average_duration_in_seconds: 4321000, date: '2019-01-02' },
]; ];
export const durationChartPlottableData = [ export const durationChartPlottableData = [
['2019-01-01', 14, '2019-01-01'], ['2019-01-01', 17, '2019-01-01'],
['2019-01-02', 50, '2019-01-02'], ['2019-01-02', 40, '2019-01-02'],
];
export const rawDurationMedianData = [
{
average_duration_in_seconds: 1234000,
date: '2018-12-01T00:00:00.000Z',
},
{
average_duration_in_seconds: 4321000,
date: '2018-12-02T00:00:00.000Z',
},
]; ];
export const pathNavIssueMetric = 172800; export const pathNavIssueMetric = 172800;
...@@ -10,7 +10,6 @@ import createFlash from '~/flash'; ...@@ -10,7 +10,6 @@ import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { import {
allowedStages as activeStages, allowedStages as activeStages,
rawDurationData,
transformedDurationData, transformedDurationData,
endpoints, endpoints,
valueStreams, valueStreams,
...@@ -64,7 +63,15 @@ describe('DurationChart actions', () => { ...@@ -64,7 +63,15 @@ describe('DurationChart actions', () => {
describe('fetchDurationData', () => { describe('fetchDurationData', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(endpoints.durationData).reply(200, [...rawDurationData]); // The first 2 stages have different duration values
mock
.onGet(endpoints.durationData)
.replyOnce(200, transformedDurationData[0].data)
.onGet(endpoints.durationData)
.replyOnce(200, transformedDurationData[1].data);
// all subsequent requests should get the same data
mock.onGet(endpoints.durationData).reply(200, transformedDurationData[2].data);
}); });
it("dispatches the 'requestDurationData' and 'receiveDurationDataSuccess' actions on success", () => { it("dispatches the 'requestDurationData' and 'receiveDurationDataSuccess' actions on success", () => {
......
import * as getters from 'ee/analytics/cycle_analytics/store/modules/duration_chart/getters'; import * as getters from 'ee/analytics/cycle_analytics/store/modules/duration_chart/getters';
import { createdAfter, createdBefore } from 'jest/cycle_analytics/mock_data'; import { createdAfter, createdBefore } from 'jest/cycle_analytics/mock_data';
import { transformedDurationData, durationChartPlottableData } from '../../../mock_data'; import {
transformedDurationData,
durationChartPlottableData as mockDurationChartPlottableData,
} from '../../../mock_data';
const rootState = { const rootState = {
createdAfter, createdAfter,
...@@ -8,25 +11,65 @@ const rootState = { ...@@ -8,25 +11,65 @@ const rootState = {
}; };
describe('DurationChart getters', () => { describe('DurationChart getters', () => {
const [selectedStage] = transformedDurationData;
const rootGetters = { isOverviewStageSelected: false };
const selectedStageDurationData = [
['2019-01-01', 13, '2019-01-01'],
['2019-01-02', 27, '2019-01-02'],
];
describe('durationChartPlottableData', () => { describe('durationChartPlottableData', () => {
it('returns plottable data for selected stages', () => { describe('with a VSA stage selected', () => {
const stateWithDurationData = { beforeEach(() => {
durationData: transformedDurationData, rootState.selectedStage = selectedStage;
}; });
it('returns plottable data for the currently selected stage', () => {
const stateWithDurationData = {
durationData: transformedDurationData,
};
expect(
getters.durationChartPlottableData(
stateWithDurationData,
getters,
rootState,
rootGetters,
),
).toEqual(selectedStageDurationData);
});
expect(getters.durationChartPlottableData(stateWithDurationData, getters, rootState)).toEqual( it('returns an empty array if there is no plottable data for the selected stages', () => {
durationChartPlottableData, const stateWithDurationData = {
); durationData: [],
};
expect(
getters.durationChartPlottableData(
stateWithDurationData,
getters,
rootState,
rootGetters,
),
).toEqual([]);
});
}); });
});
it('returns an empty array if there is no plottable data for the selected stages', () => { describe('with the overview stage selected', () => {
beforeEach(() => {
rootGetters.isOverviewStageSelected = true;
});
it('returns plottable data for all available stages', () => {
const stateWithDurationData = { const stateWithDurationData = {
durationData: [], durationData: transformedDurationData,
isOverviewStageSelected: true,
}; };
expect(getters.durationChartPlottableData(stateWithDurationData, getters, rootState)).toEqual( expect(
[], getters.durationChartPlottableData(stateWithDurationData, getters, rootState, rootGetters),
); ).toEqual(mockDurationChartPlottableData);
}); });
}); });
}); });
...@@ -10880,15 +10880,12 @@ msgstr "" ...@@ -10880,15 +10880,12 @@ msgstr ""
msgid "CycleAnalytics|All stages" msgid "CycleAnalytics|All stages"
msgstr "" msgstr ""
msgid "CycleAnalytics|Average days to completion" msgid "CycleAnalytics|Average time to completion"
msgstr "" msgstr ""
msgid "CycleAnalytics|Date" msgid "CycleAnalytics|Date"
msgstr "" msgstr ""
msgid "CycleAnalytics|Days to completion"
msgstr ""
msgid "CycleAnalytics|Display chart filters" msgid "CycleAnalytics|Display chart filters"
msgstr "" msgstr ""
...@@ -10926,18 +10923,33 @@ msgstr "" ...@@ -10926,18 +10923,33 @@ msgstr ""
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}" msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
msgstr "" msgstr ""
msgid "CycleAnalytics|Stage time: %{title}"
msgstr ""
msgid "CycleAnalytics|Stages" msgid "CycleAnalytics|Stages"
msgstr "" msgstr ""
msgid "CycleAnalytics|Tasks by type" msgid "CycleAnalytics|Tasks by type"
msgstr "" msgstr ""
msgid "CycleAnalytics|The average time spent in the selected stage for the items that were completed on each date. Data limited to the last 500 items." msgid "CycleAnalytics|The average time items spent in this stage. Data limited to items completed within this date range."
msgstr "" msgstr ""
msgid "CycleAnalytics|The given date range is larger than 180 days" msgid "CycleAnalytics|The given date range is larger than 180 days"
msgstr "" msgstr ""
msgid "CycleAnalytics|The total time items spent across each value stream stage. Data limited to items completed within this date range."
msgstr ""
msgid "CycleAnalytics|There is no data for 'Stage time' available. Adjust the current filters."
msgstr ""
msgid "CycleAnalytics|There is no data for 'Total time' available. Adjust the current filters."
msgstr ""
msgid "CycleAnalytics|Total time"
msgstr ""
msgid "CycleAnalytics|Type of work" msgid "CycleAnalytics|Type of work"
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